diff --git a/java/compiler/impl/src/com/intellij/openapi/projectRoots/impl/MockJdkWrapper.java b/java/compiler/impl/src/com/intellij/openapi/projectRoots/impl/MockJdkWrapper.java index 7c8a77432456..5cf32bef72a9 100644 --- a/java/compiler/impl/src/com/intellij/openapi/projectRoots/impl/MockJdkWrapper.java +++ b/java/compiler/impl/src/com/intellij/openapi/projectRoots/impl/MockJdkWrapper.java @@ -24,15 +24,16 @@ import com.intellij.openapi.projectRoots.SdkAdditionalData; import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.projectRoots.SdkType; import com.intellij.openapi.roots.RootProvider; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; -import org.jetbrains.annotations.NotNull; - /** * used to override JdkHome location in order to provide correct paths */ @@ -70,6 +71,14 @@ public final class MockJdkWrapper implements Sdk { return myDelegate.getRootProvider(); } + public T getUserData(@NotNull Key key) { + return myDelegate.getUserData(key); + } + + public void putUserData(@NotNull Key key, @Nullable T value) { + myDelegate.putUserData(key, value); + } + public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } diff --git a/java/java-impl/src/com/intellij/application/options/JavaDocFormattingPanel.java b/java/java-impl/src/com/intellij/application/options/JavaDocFormattingPanel.java index 565e1d79f74b..41f628635835 100644 --- a/java/java-impl/src/com/intellij/application/options/JavaDocFormattingPanel.java +++ b/java/java-impl/src/com/intellij/application/options/JavaDocFormattingPanel.java @@ -146,6 +146,11 @@ private static void setEnabled(JComponent c, boolean enabled) { return StdFileTypes.JAVA; } + @Override + protected void onLanguageChange(Language language) { + // Ignore, JavaDoc does not support languages other than Java + } + public void setPanelLanguage(Language language) { super.setPanelLanguage(Language.findInstance(JavaLanguage.class)); } diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/RefCountHolder.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/RefCountHolder.java index 2a99de2e749c..e0a8badbc009 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/RefCountHolder.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/RefCountHolder.java @@ -49,7 +49,7 @@ public class RefCountHolder { private enum State { VIRGIN, // just created or cleared BEING_WRITTEN_BY_GHP, // general highlighting pass is storing references during analysis - READY, // may be used for higlighting unused stuff + READY, // may be used for highlighting unused stuff BEING_USED_BY_PHP, // post highlighting pass is retrieving info } diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java index 514c3cda7d2b..1972758db60e 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/analysis/HighlightMethodUtil.java @@ -780,6 +780,7 @@ public class HighlightMethodUtil { int methodCount = 0; final PsiMethod[] methodsByName = aClass.findMethodsByName(method.getName(), false); for (PsiMethod other : methodsByName) { + if (other instanceof ExternallyDefinedPsiElement) continue; if (other == method || other.isConstructor() == method.isConstructor() && other.getSignature(PsiSubstitutor.EMPTY).equals(methodSignature)) { methodCount++; diff --git a/java/java-impl/src/com/intellij/psi/impl/PsiClassImplUtil.java b/java/java-impl/src/com/intellij/psi/impl/PsiClassImplUtil.java index 096f1efe8709..c7b5702785e1 100644 --- a/java/java-impl/src/com/intellij/psi/impl/PsiClassImplUtil.java +++ b/java/java-impl/src/com/intellij/psi/impl/PsiClassImplUtil.java @@ -557,10 +557,6 @@ public class PsiClassImplUtil { if (!processor.execute(field, state)) return false; } } - - for (PsiField field : PsiAugmentProvider.collectAugments(aClass, PsiField.class)) { - if (!processor.execute(field, state)) return false; - } } if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclaractionKind.METHOD)) { @@ -573,10 +569,6 @@ public class PsiClassImplUtil { } if (!processor.execute(method, state)) return false; } - - for (PsiMethod method : PsiAugmentProvider.collectAugments(aClass, PsiMethod.class)) { - if (!processor.execute(method, state)) return false; - } } if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclaractionKind.CLASS)) { diff --git a/java/java-impl/src/com/intellij/psi/impl/PsiImplUtil.java b/java/java-impl/src/com/intellij/psi/impl/PsiImplUtil.java index 432e5877843b..a6aac707d892 100644 --- a/java/java-impl/src/com/intellij/psi/impl/PsiImplUtil.java +++ b/java/java-impl/src/com/intellij/psi/impl/PsiImplUtil.java @@ -66,11 +66,6 @@ public class PsiImplUtil { if (method.isConstructor()) constructorsList.add(method); } - final List augments = PsiAugmentProvider.collectAugments(aClass, PsiMethod.class); - for (final PsiMethod method : augments) { - if (method.isConstructor()) constructorsList.add(method); - } - return constructorsList.toArray(new PsiMethod[constructorsList.size()]); } diff --git a/java/java-impl/src/com/intellij/psi/impl/source/ClassInnerStuffCache.java b/java/java-impl/src/com/intellij/psi/impl/source/ClassInnerStuffCache.java index 3697d0e8bd8a..70160b6c3430 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/ClassInnerStuffCache.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/ClassInnerStuffCache.java @@ -15,122 +15,201 @@ */ package com.intellij.psi.impl.source; +import com.intellij.openapi.util.ModificationTracker; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiField; +import com.intellij.psi.PsiMember; import com.intellij.psi.PsiMethod; +import com.intellij.psi.augment.PsiAugmentProvider; import com.intellij.psi.impl.PsiClassImplUtil; import com.intellij.psi.impl.PsiImplUtil; +import com.intellij.psi.util.CachedValue; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.PsiModificationTracker; +import com.intellij.util.ArrayUtil; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; public class ClassInnerStuffCache { - private volatile Map myCachedFieldsMap = null; - private volatile Map myCachedMethodsMap = null; - private volatile Map myCachedInnersMap = null; - - private volatile PsiMethod[] myCachedConstructors = null; private final PsiClass myClass; + private final MyModificationTracker myTreeChangeTracker; + + private CachedValue myConstructorsCache; + private CachedValue myFieldsCache; + private CachedValue myMethodsCache; + private CachedValue>> myFieldsMapCache; + private CachedValue>> myMethodsMapCache; + private CachedValue> myInnerClassesMapCache; public ClassInnerStuffCache(final PsiClass aClass) { myClass = aClass; - } - - public void dropCaches() { - myCachedConstructors = null; - - myCachedFieldsMap = null; - myCachedMethodsMap = null; - myCachedInnersMap = null; + myTreeChangeTracker = new MyModificationTracker(); + buildCaches(); } @NotNull public PsiMethod[] getConstructors() { - PsiMethod[] constructors = myCachedConstructors; - if (constructors == null) { - myCachedConstructors = constructors = PsiImplUtil.getConstructors(myClass); - } - return constructors; + final PsiMethod[] constructors = myConstructorsCache.getValue(); + return constructors != null ? constructors : PsiMethod.EMPTY_ARRAY; } - public PsiField findFieldByName(String name, boolean checkBases) { + @NotNull + public PsiField[] getFields() { + final PsiField[] fields = myFieldsCache.getValue(); + return fields != null ? fields : PsiField.EMPTY_ARRAY; + } + + @NotNull + public PsiMethod[] getMethods() { + final PsiMethod[] methods = myMethodsCache.getValue(); + return methods != null ? methods : PsiMethod.EMPTY_ARRAY; + } + + @Nullable + public PsiField findFieldByName(final String name, final boolean checkBases) { if (!checkBases) { - Map cachedFields = myCachedFieldsMap; - if (cachedFields == null) { - final PsiField[] fields = myClass.getFields(); - if (fields.length > 0) { - cachedFields = new THashMap(); - for (final PsiField field : fields) { - cachedFields.put(field.getName(), field); - } - myCachedFieldsMap = cachedFields; - } - else { - myCachedFieldsMap = Collections.emptyMap(); - return null; - } + final Map> cachedFields = myFieldsMapCache.getValue(); + if (cachedFields != null) { + final List fields = cachedFields.get(name); + return fields != null ? fields.get(0) : null; } - return cachedFields.get(name); + return null; } return PsiClassImplUtil.findFieldByName(myClass, name, checkBases); } - @NotNull - public PsiMethod[] findMethodsByName(String name, boolean checkBases) { - if(!checkBases){ - Map cachedMethods = myCachedMethodsMap; - if(cachedMethods == null){ - cachedMethods = new THashMap(); - - Map> cachedMethodsMap = new THashMap>(); - final PsiMethod[] methods = myClass.getMethods(); - for (final PsiMethod method : methods) { - List list = cachedMethodsMap.get(method.getName()); - if (list == null) { - list = new ArrayList(1); - cachedMethodsMap.put(method.getName(), list); - } - list.add(method); + public PsiMethod[] findMethodsByName(final String name, final boolean checkBases) { + if (!checkBases) { + final Map> cachedMethods = myMethodsMapCache.getValue(); + if (cachedMethods != null) { + final List methods = cachedMethods.get(name); + if (methods != null && methods.size() > 0) { + return methods.toArray(new PsiMethod[methods.size()]); } - for (Map.Entry> entry : cachedMethodsMap.entrySet()) { - List cached = entry.getValue(); - String methodName = entry.getKey(); - cachedMethods.put(methodName, cached.toArray(new PsiMethod[cached.size()])); - } - myCachedMethodsMap = cachedMethods; } - - final PsiMethod[] psiMethods = cachedMethods.get(name); - return psiMethods != null ? psiMethods : PsiMethod.EMPTY_ARRAY; + return PsiMethod.EMPTY_ARRAY; } - return PsiClassImplUtil.findMethodsByName(myClass, name, checkBases); } - public PsiClass findInnerClassByName(String name, boolean checkBases) { + @Nullable + public PsiClass findInnerClassByName(final String name, final boolean checkBases) { if (!checkBases) { - Map inners = myCachedInnersMap; - if (inners == null) { - final PsiClass[] classes = myClass.getInnerClasses(); - if (classes.length > 0) { - inners = new THashMap(); - for (final PsiClass psiClass : classes) { - inners.put(psiClass.getName(), psiClass); - } - myCachedInnersMap = inners; - } - else { - myCachedInnersMap = Collections.emptyMap(); - return null; - } - } - return inners.get(name); + final Map inners = myInnerClassesMapCache.getValue(); + return inners != null ? inners.get(name) : null; } return PsiClassImplUtil.findInnerByName(myClass, name, checkBases); } + + private void buildCaches() { + final CachedValuesManager manager = CachedValuesManager.getManager(myClass.getProject()); + final Object[] dependencies = {PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT, myTreeChangeTracker}; + + myConstructorsCache = manager.createCachedValue(new CachedValueProvider() { + public Result compute() { + return Result.create(PsiImplUtil.getConstructors(myClass), dependencies); + } + }, false); + + myFieldsCache = manager.createCachedValue(new CachedValueProvider() { + public Result compute() { + return Result.create(getAllFields(), dependencies); + } + }, false); + + myMethodsCache = manager.createCachedValue(new CachedValueProvider() { + public Result compute() { + return Result.create(getAllMethods(), dependencies); + } + }, false); + + myFieldsMapCache = manager.createCachedValue(new CachedValueProvider>>() { + public Result>> compute() { + return Result.create(getFieldsMap(), dependencies); + } + }, false); + + myMethodsMapCache = manager.createCachedValue(new CachedValueProvider>>() { + public Result>> compute() { + return Result.create(getMethodsMap(), dependencies); + } + }, false); + + myInnerClassesMapCache = manager.createCachedValue(new CachedValueProvider>() { + public Result> compute() { + return Result.create(getInnerClassesMap(), dependencies); + } + }, false); + } + + private PsiField[] getAllFields() { + if (!(myClass instanceof PsiClassImpl)) return myClass.getFields(); + + final PsiField[] own = ((PsiClassImpl)myClass).getStubOrPsiChildren(Constants.FIELD_BIT_SET, PsiField.ARRAY_FACTORY); + final List ext = PsiAugmentProvider.collectAugments(myClass, PsiField.class); + return ArrayUtil.mergeArrayAndCollection(own, ext, PsiField.ARRAY_FACTORY); + } + + private PsiMethod[] getAllMethods() { + if (!(myClass instanceof PsiClassImpl)) return myClass.getMethods(); + + final PsiMethod[] own = ((PsiClassImpl)myClass).getStubOrPsiChildren(Constants.METHOD_BIT_SET, PsiMethod.ARRAY_FACTORY); + final List ext = PsiAugmentProvider.collectAugments(myClass, PsiMethod.class); + return ArrayUtil.mergeArrayAndCollection(own, ext, PsiMethod.ARRAY_FACTORY); + } + + @Nullable + private Map> getFieldsMap() { + return getMembersMap(getFields()); + } + + @Nullable + private Map> getMethodsMap() { + return getMembersMap(getMethods()); + } + + @Nullable + private static Map> getMembersMap(final T[] members) { + if (members.length == 0) return null; + + final Map> cachedMembers = new THashMap>(); + for (final T member : members) { + List list = cachedMembers.get(member.getName()); + if (list == null) { + cachedMembers.put(member.getName(), (list = new ArrayList(1))); + } + list.add(member); + } + return cachedMembers; + } + + @Nullable + private Map getInnerClassesMap() { + final PsiClass[] classes = myClass.getInnerClasses(); + if (classes.length == 0) return null; + + final Map cachedInners = new THashMap(); + for (final PsiClass psiClass : classes) { + cachedInners.put(psiClass.getName(), psiClass); + } + return cachedInners; + } + + public void dropCaches() { + myTreeChangeTracker.myCount++; + } + + private static class MyModificationTracker implements ModificationTracker { + private long myCount = 0; + public long getModificationCount() { + return myCount; + } + } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/psi/impl/source/PsiClassImpl.java b/java/java-impl/src/com/intellij/psi/impl/source/PsiClassImpl.java index 977a9b40561f..8cf1bcdc9619 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/PsiClassImpl.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/PsiClassImpl.java @@ -64,7 +64,7 @@ import java.util.Map; public class PsiClassImpl extends JavaStubPsiElement> implements PsiClass, PsiQualifiedNamedElement, Queryable { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PsiClassImpl"); - private final ClassInnerStuffCache innersCache = new ClassInnerStuffCache(this); + private final ClassInnerStuffCache myInnersCache = new ClassInnerStuffCache(this); private PsiMethod myValuesMethod = null; private PsiMethod myValueOfMethod = null; @@ -97,7 +97,7 @@ public class PsiClassImpl extends JavaStubPsiElement> implements } private void dropCaches() { - innersCache.dropCaches(); + myInnersCache.dropCaches(); myCachedForLongName = null; } @@ -306,17 +306,17 @@ public class PsiClassImpl extends JavaStubPsiElement> implements @NotNull public PsiField[] getFields() { - return getStubOrPsiChildren(Constants.FIELD_BIT_SET, PsiField.ARRAY_FACTORY); + return myInnersCache.getFields(); } @NotNull public PsiMethod[] getMethods() { - return getStubOrPsiChildren(Constants.METHOD_BIT_SET, PsiMethod.ARRAY_FACTORY); + return myInnersCache.getMethods(); } @NotNull public PsiMethod[] getConstructors() { - return innersCache.getConstructors(); + return myInnersCache.getConstructors(); } @NotNull @@ -350,7 +350,7 @@ public class PsiClassImpl extends JavaStubPsiElement> implements } public PsiField findFieldByName(String name, boolean checkBases) { - return innersCache.findFieldByName(name, checkBases); + return myInnersCache.findFieldByName(name, checkBases); } public PsiMethod findMethodBySignature(PsiMethod patternMethod, boolean checkBases) { @@ -364,7 +364,7 @@ public class PsiClassImpl extends JavaStubPsiElement> implements @NotNull public PsiMethod[] findMethodsByName(String name, boolean checkBases) { - return innersCache.findMethodsByName(name, checkBases); + return myInnersCache.findMethodsByName(name, checkBases); } @NotNull @@ -378,7 +378,7 @@ public class PsiClassImpl extends JavaStubPsiElement> implements } public PsiClass findInnerClassByName(String name, boolean checkBases) { - return innersCache.findInnerClassByName(name, checkBases); + return myInnersCache.findInnerClassByName(name, checkBases); } public PsiTypeParameterList getTypeParameterList() { diff --git a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportHelper.java b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportHelper.java index 04d4e1bc33d4..e262972b78ba 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportHelper.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportHelper.java @@ -644,6 +644,7 @@ public class ImportHelper{ while (!stack.isEmpty()) { final PsiElement child = stack.removeFirst(); if (child instanceof PsiImportList) continue; + if (child instanceof PsiLiteralExpression) continue; stack.addAll(Arrays.asList(child.getChildren())); for(final PsiReference reference : child.getReferences()){ diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/EnumTypeConversionRule.java b/java/java-impl/src/com/intellij/refactoring/extractclass/EnumTypeConversionRule.java new file mode 100644 index 000000000000..61c65355bd74 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/EnumTypeConversionRule.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * User: anna + * Date: 07-Jun-2010 + */ +package com.intellij.refactoring.extractclass; + +import com.intellij.psi.*; +import com.intellij.psi.search.PsiSearchScopeUtil; +import com.intellij.psi.search.SearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.rules.TypeConversionRule; + +import java.util.List; + +public class EnumTypeConversionRule extends TypeConversionRule { + private final List myEnumConstants; + + public EnumTypeConversionRule(List enumConstants) { + myEnumConstants = enumConstants; + } + + @Override + public TypeConversionDescriptorBase findConversion(PsiType from, + PsiType to, + PsiMember member, + PsiExpression context, + TypeMigrationLabeler labeler) { + final PsiMethodCallExpression callExpression = PsiTreeUtil.getParentOfType(context, PsiMethodCallExpression.class, false); + if (callExpression != null) { + final PsiMethod resolved = callExpression.resolveMethod(); + if (resolved != null) { + final SearchScope searchScope = labeler.getRules().getSearchScope(); + if (!PsiSearchScopeUtil.isInScope(searchScope, resolved)) { + return null; + } + } + } + final PsiField field = PsiTreeUtil.getParentOfType(context, PsiField.class); + if (field != null && + !myEnumConstants.contains(field) && + field.hasModifierProperty(PsiModifier.STATIC) && + field.hasModifierProperty(PsiModifier.FINAL) && + field.hasInitializer()) { + return null; + } + final PsiClass toClass = PsiUtil.resolveClassInType(to); + if (toClass != null && toClass.isEnum()) { + final PsiMethod[] constructors = toClass.getConstructors(); + if (constructors.length == 1) { + final PsiMethod constructor = constructors[0]; + final PsiParameter[] parameters = constructor.getParameterList().getParameters(); + if (parameters.length == 1) { + if (TypeConversionUtil.isAssignable(parameters[0].getType(), from)) { + return new TypeConversionDescriptorBase(); + } + } + } + } + return null; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassDialog.java b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassDialog.java index 36d58e364cce..e3a1b5960feb 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassDialog.java +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassDialog.java @@ -245,7 +245,7 @@ class ExtractClassDialog extends RefactoringDialog implements MemberInfoChangeLi protected JComponent createCenterPanel() { final JPanel panel = new JPanel(new BorderLayout()); final MemberSelectionPanel memberSelectionPanel = - new MemberSelectionPanel(RefactorJBundle.message("members.to.extract.label"), memberInfo, "as enum"){ + new MemberSelectionPanel(RefactorJBundle.message("members.to.extract.label"), memberInfo, "As enum"){ @Override protected MemberSelectionTable createMemberSelectionTable(final List memberInfo, String abstractColumnHeader) { return new MemberSelectionTable(memberInfo, abstractColumnHeader) { @@ -342,7 +342,7 @@ class ExtractClassDialog extends RefactoringDialog implements MemberInfoChangeLi private static boolean isConstantField(PsiMember member) { return member instanceof PsiField && member.hasModifierProperty(PsiModifier.STATIC) && - member.hasModifierProperty(PsiModifier.FINAL) && + // member.hasModifierProperty(PsiModifier.FINAL) && ((PsiField)member).hasInitializer(); } diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassProcessor.java b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassProcessor.java index c411f3de9d5e..912b44ab088a 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassProcessor.java +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractClassProcessor.java @@ -16,10 +16,12 @@ package com.intellij.refactoring.extractclass; import com.intellij.ide.util.PackageUtil; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtil; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; @@ -43,7 +45,6 @@ import com.intellij.refactoring.psi.MethodInheritanceUtils; import com.intellij.refactoring.psi.TypeParametersVisitor; import com.intellij.refactoring.util.FixableUsageInfo; import com.intellij.refactoring.util.FixableUsagesRefactoringProcessor; -import com.intellij.refactoring.util.RefactoringUIUtil; import com.intellij.refactoring.util.RefactoringUtil; import com.intellij.refactoring.util.classMembers.MemberInfo; import com.intellij.usageView.UsageInfo; @@ -73,6 +74,8 @@ public class ExtractClassProcessor extends FixableUsagesRefactoringProcessor { private final String delegateFieldName; private final boolean requiresBackpointer; private boolean delegationRequired = false; + private ExtractEnumProcessor myExtractEnumProcessor; + private PsiClass myClass; public ExtractClassProcessor(PsiClass sourceClass, List fields, @@ -122,11 +125,25 @@ public class ExtractClassProcessor extends FixableUsagesRefactoringProcessor { } typeParams.addAll(typeParamSet); } + myClass = ApplicationManager.getApplication().runWriteAction( + new Computable() { + public PsiClass compute() { + return buildClass(); + } + } + ); + myExtractEnumProcessor = new ExtractEnumProcessor(myProject, this.enumConstants, fields, myClass); } @Override protected boolean preprocessUsages(final Ref refUsages) { final MultiMap conflicts = new MultiMap(); + myExtractEnumProcessor.findEnumConstantConflicts(refUsages, conflicts); + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + myClass.delete(); + } + }); final Project project = sourceClass.getProject(); final GlobalSearchScope scope = GlobalSearchScope.allScope(project); final PsiClass existingClass = @@ -153,21 +170,7 @@ public class ExtractClassProcessor extends FixableUsagesRefactoringProcessor { conflicts.putValue(field, "Field \'" + field.getName() + "\' needs setter"); } } - for (final PsiField enumConstant : enumConstants) { - final PsiExpression initializer = enumConstant.getInitializer(); - assert initializer != null; - initializer.accept(new JavaRecursiveElementWalkingVisitor(){ - @Override - public void visitReferenceExpression(PsiReferenceExpression expression) { - super.visitReferenceExpression(expression); - final PsiElement resolved = expression.resolve(); - if (!enumConstants.contains(resolved) && fields.contains(resolved)) { - conflicts.putValue(initializer, "Enum constant " + RefactoringUIUtil.getDescription(enumConstant, false) + - " would forward reference on field " + RefactoringUIUtil.getDescription(resolved, false)); - } - } - }); - } + checkConflicts(refUsages, conflicts); return showConflicts(conflicts, refUsages.get()); } @@ -245,6 +248,7 @@ public class ExtractClassProcessor extends FixableUsagesRefactoringProcessor { if (delegationRequired) { buildDelegate(); } + myExtractEnumProcessor.performEnumConstantTypeMigration(usageInfos); final Set members = new HashSet(); for (PsiMethod method : methods) { final PsiMethod member = psiClass.findMethodBySignature(method, false); @@ -470,6 +474,7 @@ public class ExtractClassProcessor extends FixableUsagesRefactoringProcessor { findUsagesForField(field, usages); usages.add(new RemoveField(field)); } + usages.addAll(myExtractEnumProcessor.findEnumConstantUsages(new ArrayList(usages))); for (PsiClass innerClass : innerClasses) { findUsagesForInnerClass(innerClass, usages); usages.add(new RemoveInnerClass(innerClass)); diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractEnumProcessor.java b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractEnumProcessor.java new file mode 100644 index 000000000000..b067455a0979 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractEnumProcessor.java @@ -0,0 +1,218 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * User: anna + * Date: 08-Jun-2010 + */ +package com.intellij.refactoring.extractclass; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PropertyUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.extractclass.usageInfo.ReplaceStaticVariableAccess; +import com.intellij.refactoring.psi.MutationUtils; +import com.intellij.refactoring.typeMigration.TypeMigrationProcessor; +import com.intellij.refactoring.typeMigration.TypeMigrationRules; +import com.intellij.refactoring.util.EnumConstantsUtil; +import com.intellij.refactoring.util.FixableUsageInfo; +import com.intellij.refactoring.util.RefactoringUIUtil; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.containers.MultiMap; + +import java.util.*; + +public class ExtractEnumProcessor { + private Project myProject; + private List myEnumConstants; + private PsiClass myClass; + + private TypeMigrationProcessor myTypeMigrationProcessor; + private List myFields; + + public ExtractEnumProcessor(Project project, List enumConstants, List fields, PsiClass aClass) { + myProject = project; + myEnumConstants = enumConstants; + myFields = fields; + myClass = aClass; + } + + + public void findEnumConstantConflicts(final Ref refUsages, final MultiMap conflicts) { + if (hasUsages2Migrate()) { + final List resolvableConflicts = new ArrayList(); + for (UsageInfo failedUsage : myTypeMigrationProcessor.getLabeler().getFailedUsages()) { + final PsiElement element = failedUsage.getElement(); + if (element instanceof PsiReferenceExpression) { + resolvableConflicts.add(new FixableUsageInfo(element) { + @Override + public void fixUsage() throws IncorrectOperationException { + final PsiReferenceExpression expression = (PsiReferenceExpression)element; + final String link = PropertyUtil.suggestGetterName("value", expression.getType()) + "()"; + MutationUtils.replaceExpression(expression.getReferenceName() + "." + link, expression); + } + }); + } else if (element != null) { + resolvableConflicts.add(new ConflictUsageInfo(element, null)); + } + } + if (!resolvableConflicts.isEmpty()) { + final List usageInfos = new ArrayList(Arrays.asList(refUsages.get())); + for (Iterator iterator = resolvableConflicts.iterator(); iterator.hasNext();) { + final UsageInfo conflict = iterator.next(); + for (UsageInfo usageInfo : usageInfos) { + if (conflict.getElement() == usageInfo.getElement()) { + iterator.remove(); + break; + } + } + } + resolvableConflicts.addAll(0, usageInfos); + refUsages.set(resolvableConflicts.toArray(new UsageInfo[resolvableConflicts.size()])); + } + } + } + + private boolean hasUsages2Migrate() { + return myTypeMigrationProcessor != null; + } + + public List findEnumConstantUsages(List fieldUsages) { + final List result = new ArrayList(); + if (!myEnumConstants.isEmpty()) { + final Set switchStatements = new HashSet(); + for (UsageInfo usage : fieldUsages) { + if (usage instanceof ReplaceStaticVariableAccess) { + final PsiElement element = usage.getElement(); + final PsiSwitchStatement switchStatement = PsiTreeUtil.getParentOfType(element, PsiSwitchStatement.class); + if (switchStatement != null) { + switchStatements.add(switchStatement); + } + } + } + + final PsiConstantEvaluationHelper evaluationHelper = + JavaPsiFacade.getInstance(myProject).getConstantEvaluationHelper(); + final Set enumValues = new HashSet(); + for (PsiField enumConstant : myEnumConstants) { + enumValues.add(evaluationHelper.computeConstantExpression(enumConstant.getInitializer())); + } + final PsiType enumValueType = myEnumConstants.get(0).getType(); + + for (PsiSwitchStatement switchStatement : switchStatements) { + final PsiStatement errStatement = EnumConstantsUtil.isEnumSwitch(switchStatement, enumValueType, enumValues); + if (errStatement != null) { + String description = null; + if (errStatement instanceof PsiSwitchLabelStatement) { + final PsiExpression caseValue = ((PsiSwitchLabelStatement)errStatement).getCaseValue(); + if (caseValue != null) { + description = caseValue.getText() + " can not be replaced with enum"; + } + } + result.add(new ConflictUsageInfo(errStatement, description)); + + } + else { + final PsiExpression expression = switchStatement.getExpression(); + if (expression instanceof PsiReferenceExpression) { + final PsiElement element = ((PsiReferenceExpression)expression).resolve(); + if (element != null) { + if (!element.getManager().isInProject(element)) { + result.add(new ConflictUsageInfo(expression, StringUtil.capitalize(RefactoringUIUtil.getDescription(element, false)) + " is out of project")); + } + } + } + else { + result.add(new ConflictUsageInfo(expression, null)); + } + } + } + + final TypeMigrationRules rules = new TypeMigrationRules(myEnumConstants.get(0).getType()); + rules.addConversionDescriptor(new EnumTypeConversionRule(myEnumConstants)); + rules.setMigrationRootType( + JavaPsiFacade.getElementFactory(myProject).createType(myClass)); + rules.setBoundScope(GlobalSearchScope.projectScope(myProject)); + myTypeMigrationProcessor = new TypeMigrationProcessor(myProject, myEnumConstants.toArray(new PsiElement[myEnumConstants.size()]), rules); + for (UsageInfo usageInfo : myTypeMigrationProcessor.findUsages()) { + final PsiElement migrateElement = usageInfo.getElement(); + if (migrateElement instanceof PsiField) { + final PsiField enumConstantField = (PsiField)migrateElement; + if (enumConstantField.hasModifierProperty(PsiModifier.STATIC) && + enumConstantField.hasModifierProperty(PsiModifier.FINAL) && + enumConstantField.hasInitializer() && + !myEnumConstants.contains(enumConstantField)) { + continue; + } + } + result.add(new EnumTypeMigrationUsageInfo(usageInfo)); + } + } + return result; + } + + public void performEnumConstantTypeMigration(UsageInfo[] usageInfos) { + if (hasUsages2Migrate()) { + final List migrationInfos = new ArrayList(); + for (UsageInfo usageInfo : usageInfos) { + if (usageInfo instanceof EnumTypeMigrationUsageInfo) { + migrationInfos.add(((EnumTypeMigrationUsageInfo)usageInfo).getUsageInfo()); + } + } + myTypeMigrationProcessor.performRefactoring(migrationInfos.toArray(new UsageInfo[migrationInfos.size()])); + } + } + + private static class EnumTypeMigrationUsageInfo extends FixableUsageInfo { + private UsageInfo myUsageInfo; + + public EnumTypeMigrationUsageInfo(UsageInfo usageInfo) { + super(usageInfo.getElement()); + myUsageInfo = usageInfo; + } + + @Override + public void fixUsage() throws IncorrectOperationException { + } + + public UsageInfo getUsageInfo() { + return myUsageInfo; + } + } + + private static class ConflictUsageInfo extends FixableUsageInfo { + private final String myDescription; + + public ConflictUsageInfo(PsiElement expression, String description) { + super(expression); + myDescription = description; + } + + @Override + public void fixUsage() throws IncorrectOperationException { + } + + @Override + public String getConflictMessage() { + return "Unable to migrate statement to enum constant." + (myDescription != null ? " " + myDescription : ""); + } + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractedClassBuilder.java b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractedClassBuilder.java index dd9d6e83fe68..37ee118bc16f 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractedClassBuilder.java +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/ExtractedClassBuilder.java @@ -259,7 +259,7 @@ class ExtractedClassBuilder { if (hasEnumConstants()) { out.append(StringUtil.join(enumConstantFields, new Function() { public String fun(PsiField field) { - final StringBuffer fieldStr = new StringBuffer(field.getName().toUpperCase() + "("); + final StringBuffer fieldStr = new StringBuffer(field.getName() + "("); final PsiExpression initializer = field.getInitializer(); if (initializer != null) { initializer.accept(new Mutator(fieldStr)); @@ -411,7 +411,7 @@ class ExtractedClassBuilder { final String name = field.getName(); if (enumConstantFields.contains(field)) { - out.append(name.toUpperCase()).append(".").append(getterName()).append("()"); + out.append(name).append(".").append(getterName()).append("()"); } else { if (qualifier != null && name.equals(expression.getReferenceName())) { out.append("this."); diff --git a/java/java-impl/src/com/intellij/refactoring/extractclass/usageInfo/ReplaceStaticVariableAccess.java b/java/java-impl/src/com/intellij/refactoring/extractclass/usageInfo/ReplaceStaticVariableAccess.java index 5479050ac62a..178244400138 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractclass/usageInfo/ReplaceStaticVariableAccess.java +++ b/java/java-impl/src/com/intellij/refactoring/extractclass/usageInfo/ReplaceStaticVariableAccess.java @@ -15,26 +15,63 @@ */ package com.intellij.refactoring.extractclass.usageInfo; -import com.intellij.psi.PsiReferenceExpression; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.psi.*; import com.intellij.psi.util.PropertyUtil; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.psi.MutationUtils; import com.intellij.refactoring.util.FixableUsageInfo; +import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; public class ReplaceStaticVariableAccess extends FixableUsageInfo { - private final PsiReferenceExpression expression; - private final String delegateClass; + private final PsiReferenceExpression expression; + private final String delegateClass; private final boolean myEnumConstant; + private static final Logger LOGGER = Logger.getInstance("#" + ReplaceStaticVariableAccess.class.getName()); public ReplaceStaticVariableAccess(PsiReferenceExpression expression, String delegateClass, boolean enumConstant) { - super(expression); - this.expression = expression; - this.delegateClass = delegateClass; + super(expression); + this.expression = expression; + this.delegateClass = delegateClass; myEnumConstant = enumConstant; } - public void fixUsage() throws IncorrectOperationException { - MutationUtils.replaceExpression(delegateClass + '.' + expression.getReferenceName() + (myEnumConstant ? "." + PropertyUtil.suggestGetterName("value", expression.getType())+ - "()" : ""), expression); + public void fixUsage() throws IncorrectOperationException { + if (myEnumConstant) { + final PsiSwitchLabelStatement switchStatement = PsiTreeUtil.getParentOfType(expression, PsiSwitchLabelStatement.class); + if (switchStatement != null) { + MutationUtils.replaceExpression(expression.getReferenceName(), expression); + return; + } } + boolean replaceWithGetEnumValue = myEnumConstant; + if (replaceWithGetEnumValue) { + final PsiMethodCallExpression callExpression = PsiTreeUtil.getParentOfType(expression, PsiMethodCallExpression.class); + if (callExpression != null) { + final PsiElement resolved = callExpression.getMethodExpression().resolve(); + if (resolved instanceof PsiMethod) { + final PsiParameter[] parameters = ((PsiMethod)resolved).getParameterList().getParameters(); + final PsiExpression[] args = callExpression.getArgumentList().getExpressions(); + final int idx = ArrayUtil.find(args, expression); + if (idx != -1 && parameters[idx].getType().getCanonicalText().equals(delegateClass)) { + replaceWithGetEnumValue = false; + } + } + } + else { + final PsiReturnStatement returnStatement = PsiTreeUtil.getParentOfType(expression, PsiReturnStatement.class); + if (returnStatement != null) { + final PsiMethod psiMethod = PsiTreeUtil.getParentOfType(expression, PsiMethod.class); + LOGGER.assertTrue(psiMethod != null); + final PsiType returnType = psiMethod.getReturnType(); + if (returnType != null && returnType.getCanonicalText().equals(delegateClass)) { + replaceWithGetEnumValue = false; + } + } + } + } + final String link = replaceWithGetEnumValue ? "." + PropertyUtil.suggestGetterName("value", expression.getType()) + "()" : ""; + MutationUtils.replaceExpression(delegateClass + '.' + expression.getReferenceName() + link, expression); + } } diff --git a/java/java-impl/src/com/intellij/refactoring/move/moveMembers/MoveJavaMemberHandler.java b/java/java-impl/src/com/intellij/refactoring/move/moveMembers/MoveJavaMemberHandler.java index ab617c81fa12..5f0ba5be7c99 100644 --- a/java/java-impl/src/com/intellij/refactoring/move/moveMembers/MoveJavaMemberHandler.java +++ b/java/java-impl/src/com/intellij/refactoring/move/moveMembers/MoveJavaMemberHandler.java @@ -84,7 +84,7 @@ public class MoveJavaMemberHandler implements MoveMemberHandler { PsiReferenceExpression refExpr = (PsiReferenceExpression)usage.reference; PsiExpression qualifier = refExpr.getQualifierExpression(); if (qualifier != null) { - if (usage.qualifierClass != null) { + if (usage.qualifierClass != null && PsiTreeUtil.getParentOfType(refExpr, PsiSwitchLabelStatement.class) == null) { changeQualifier(refExpr, usage.qualifierClass, usage.member); } else { @@ -92,7 +92,7 @@ public class MoveJavaMemberHandler implements MoveMemberHandler { } } else { // no qualifier - if (usage.qualifierClass != null) { + if (usage.qualifierClass != null && PsiTreeUtil.getParentOfType(refExpr, PsiSwitchLabelStatement.class) == null) { changeQualifier(refExpr, usage.qualifierClass, usage.member); } } diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ChangeTypeSignatureHandler.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ChangeTypeSignatureHandler.java new file mode 100644 index 000000000000..7ab9273ea2d8 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ChangeTypeSignatureHandler.java @@ -0,0 +1,70 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.codeInsight.TargetElementUtilBase; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.RefactoringActionHandler; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.typeMigration.ui.TypeMigrationDialog; +import com.intellij.refactoring.util.CommonRefactoringUtil; +import org.jetbrains.annotations.NotNull; + +public class ChangeTypeSignatureHandler implements RefactoringActionHandler { + private static final Logger LOG = Logger.getInstance("#" + ChangeTypeSignatureHandler.class.getName()); + + public static final String REFACTORING_NAME = "Type Migration"; + + public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) { + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + final int offset = TargetElementUtilBase.adjustOffset(editor.getDocument(), editor.getCaretModel().getOffset()); + final PsiElement element = file.findElementAt(offset); + PsiTypeElement typeElement = PsiTreeUtil.getParentOfType(element, PsiTypeElement.class); + while (typeElement != null) { + final PsiElement parent = typeElement.getParent(); + if (parent instanceof PsiVariable || parent instanceof PsiMember || (parent instanceof PsiReferenceParameterList && PsiTreeUtil.getParentOfType(parent, PsiMember.class) instanceof PsiClass)) { + invoke(project, parent, null, editor); + return; + } + typeElement = PsiTreeUtil.getParentOfType(parent, PsiTypeElement.class, false); + } + CommonRefactoringUtil.showErrorHint(project, editor, + "The caret should be positioned on type of field, variable, method or method parameter to be refactored", + REFACTORING_NAME, "refactoring.migrateType"); + } + + + public void invoke(@NotNull final Project project, @NotNull final PsiElement[] elements, final DataContext dataContext) { + LOG.assertTrue(elements.length == 1); + final PsiElement element = elements[0]; + invokeOnElement(project, element); + } + + public static boolean invokeOnElement(final Project project, final PsiElement element) { + if (element instanceof PsiVariable || element instanceof PsiMember || element instanceof PsiFile) { + invoke(project, element, null, null); + return true; + } + if (element instanceof PsiReferenceParameterList && PsiTreeUtil.getParentOfType(element, PsiMember.class) instanceof PsiClass) { + invoke(project, element, null, null); + return true; + } + return false; + } + + public static void invoke(final Project project, final PsiElement root, final TypeMigrationRules rules, final Editor editor) { + if (Util.canBeMigrated(root)) { + TypeMigrationDialog dialog = new TypeMigrationDialog(project, root, rules); + dialog.show(); + return; + } + + CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.message("only.fields.variables.of.methods.of.valid.type.can.be.considered"), + RefactoringBundle.message("unable.to.start.type.migration"), null); + + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ClassTypeArgumentMigrationProcessor.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ClassTypeArgumentMigrationProcessor.java new file mode 100644 index 000000000000..999a9d6f3bb1 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ClassTypeArgumentMigrationProcessor.java @@ -0,0 +1,185 @@ +/* + * User: anna + * Date: 19-Apr-2008 + */ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.*; +import com.intellij.psi.search.searches.OverridingMethodsSearch; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.util.Query; + +import java.util.*; + +public class ClassTypeArgumentMigrationProcessor { + private static final Logger LOG = Logger.getInstance("#" + ClassTypeArgumentMigrationProcessor.class.getName()); + + private final TypeMigrationLabeler myLabeler; + + public ClassTypeArgumentMigrationProcessor(final TypeMigrationLabeler labeler) { + myLabeler = labeler; + } + + public void migrateClassTypeParameter(final PsiReferenceParameterList referenceParameterList, final PsiType migrationType) { + final PsiClass psiClass = PsiTreeUtil.getParentOfType(referenceParameterList, PsiClass.class); + LOG.assertTrue(psiClass != null); + + final PsiClass superClass = psiClass.getSuperClass(); + LOG.assertTrue(superClass != null); + + myLabeler.getTypeEvaluator().setType(new TypeMigrationUsageInfo(superClass), migrationType); + + + final Map> roots = new HashMap>(); + + markTypeParameterUsages(psiClass, migrationType, referenceParameterList, roots); + + final Set processed = new HashSet(); + for (Map.Entry> entry : roots.entrySet()) { + final PsiElement member = entry.getKey(); + final PsiType type = entry.getValue().second; + + if (member instanceof PsiParameter && ((PsiParameter)member).getDeclarationScope() instanceof PsiMethod) { + myLabeler.migrateMethodCallExpressions(type, (PsiParameter)member, psiClass); + } + + + final PsiReference[] references = entry.getValue().first; + for (PsiReference usage : references) { + myLabeler.migrateRootUsageExpression(usage, processed); + } + } + } + + private void markTypeParameterUsages(final PsiClass psiClass, PsiType migrationType, PsiReferenceParameterList referenceParameterList, + final Map> roots) { + + final Map visibleTypeParams = getTypeParametersHierarchy(referenceParameterList); + final PsiSubstitutor substitutor = composeSubstitutor(psiClass.getProject(), migrationType, visibleTypeParams); + for (Map.Entry entry : visibleTypeParams.entrySet()) { + final TypeParameterSearcher parameterSearcher = new TypeParameterSearcher(entry.getValue()); + entry.getKey().accept(new JavaRecursiveElementVisitor(){ + @Override + public void visitMethod(final PsiMethod method) { + super.visitMethod(method); + processMemberType(method, parameterSearcher, psiClass, substitutor, roots); + for (PsiParameter parameter : method.getParameterList().getParameters()) { + processMemberType(parameter, parameterSearcher, psiClass, substitutor, roots); + } + } + + @Override + public void visitField(final PsiField field) { + super.visitField(field); + processMemberType(field, parameterSearcher, psiClass, substitutor, roots); + } + }); + } + } + + private void processMemberType(final PsiElement element, + final TypeParameterSearcher parameterSearcher, + final PsiClass psiClass, + final PsiSubstitutor substitutor, + final Map> roots) { + final PsiType elementType = TypeMigrationLabeler.getElementType(element); + if (elementType != null && elementType.accept(parameterSearcher).booleanValue()) { + final PsiType memberType = substitutor.substitute(elementType); + + prepareMethodsChangeSignature(psiClass, element, memberType); + + final List refs = TypeMigrationLabeler.filterReferences(psiClass, ReferencesSearch.search(element, psiClass.getUseScope())); + + roots.put(element, Pair.create(myLabeler.markRootUsages(element, memberType, refs.toArray(new PsiReference[refs.size()])), memberType)); + } + } + + private static PsiSubstitutor composeSubstitutor(final Project project, final PsiType migrationType, final Map visibleTypeParams) { + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; + final PsiResolveHelper psiResolveHelper = JavaPsiFacade.getInstance(project).getResolveHelper(); + for (Map.Entry entry : visibleTypeParams.entrySet()) { + final PsiClassType clearedOriginalType = JavaPsiFacade.getElementFactory(project).createType(entry.getKey(), PsiSubstitutor.EMPTY); + for (PsiTypeParameter parameter : entry.getValue()) { + substitutor = substitutor.put(parameter, + psiResolveHelper.getSubstitutionForTypeParameter(parameter, clearedOriginalType, migrationType, true, clearedOriginalType.getLanguageLevel())); + } + } + return substitutor; + } + + private static Map getTypeParametersHierarchy(final PsiReferenceParameterList referenceParameterList) { + final PsiElement parent = referenceParameterList.getParent(); + LOG.assertTrue(parent instanceof PsiJavaCodeReferenceElement); + final PsiClass superClass = (PsiClass)((PsiJavaCodeReferenceElement)parent).resolve(); + LOG.assertTrue(superClass != null); + + final Map visibleTypeParams = new HashMap(); + visibleTypeParams.put(superClass, superClass.getTypeParameters()); + + final HashSet superClasses = new HashSet(); + InheritanceUtil.getSuperClasses(superClass, superClasses, true); + for (PsiClass superSuperClass : superClasses) { + visibleTypeParams.put(superSuperClass, superSuperClass.getTypeParameters()); + } + return visibleTypeParams; + } + + /** + * signature should be changed for methods with type parameters + */ + private void prepareMethodsChangeSignature(final PsiClass currentClass, final PsiElement memberToChangeSignature, final PsiType memberType) { + if (memberToChangeSignature instanceof PsiMethod) { + final Query methodQuery = OverridingMethodsSearch.search(((PsiMethod)memberToChangeSignature), currentClass.getUseScope(), true); + for (PsiMethod method : methodQuery) { + myLabeler.addRoot(new TypeMigrationUsageInfo(method), memberType, method, false); + } + } else if (memberToChangeSignature instanceof PsiParameter && ((PsiParameter)memberToChangeSignature).getDeclarationScope() instanceof PsiMethod) { + final PsiMethod superMethod = (PsiMethod)((PsiParameter)memberToChangeSignature).getDeclarationScope(); + final int parameterIndex = superMethod.getParameterList().getParameterIndex((PsiParameter)memberToChangeSignature); + final Query methodQuery = OverridingMethodsSearch.search(superMethod, currentClass.getUseScope(), true); + for (PsiMethod method : methodQuery) { + final PsiParameter parameter = method.getParameterList().getParameters()[parameterIndex]; + myLabeler.addRoot(new TypeMigrationUsageInfo(parameter), memberType, parameter, false); + } + } + } + + private static class TypeParameterSearcher extends PsiTypeVisitor { + private final Set myTypeParams = new HashSet(); + + private TypeParameterSearcher(final PsiTypeParameter[] set) { + myTypeParams.addAll(Arrays.asList(set)); + } + + public Boolean visitType(final PsiType type) { + return false; + } + + public Boolean visitArrayType(final PsiArrayType arrayType) { + return arrayType.getComponentType().accept(this); + } + + public Boolean visitClassType(final PsiClassType classType) { + final PsiClass aClass = classType.resolve(); + if (aClass instanceof PsiTypeParameter && myTypeParams.contains((PsiTypeParameter)aClass)) return true; + + final PsiType[] types = classType.getParameters(); + for (final PsiType psiType : types) { + if (psiType.accept(this).booleanValue()) return true; + } + return false; + } + + public Boolean visitWildcardType(final PsiWildcardType wildcardType) { + final PsiType bound = wildcardType.getBound(); + return bound != null && bound.accept(this).booleanValue(); + } + } + +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeConversionDescriptorBase.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeConversionDescriptorBase.java new file mode 100644 index 000000000000..a4266a8171ab --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeConversionDescriptorBase.java @@ -0,0 +1,27 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.psi.PsiExpression; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; + +public class TypeConversionDescriptorBase { + + private TypeMigrationUsageInfo myRoot; + + public TypeConversionDescriptorBase() { + } + + public TypeMigrationUsageInfo getRoot() { + return myRoot; + } + + public void setRoot(final TypeMigrationUsageInfo root) { + myRoot = root; + } + + public void replace(PsiExpression expression){} + + @Override + public String toString() { + return "$"; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeEvaluator.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeEvaluator.java new file mode 100644 index 000000000000..93f340b74cb0 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeEvaluator.java @@ -0,0 +1,515 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiImplUtil; +import com.intellij.psi.impl.source.tree.java.PsiBinaryExpressionImpl; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.util.Function; +import com.intellij.util.containers.HashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Map; + +/** + * Created by IntelliJ IDEA. + * User: db + * Date: 27.06.2003 + * Time: 22:48:08 + * To change this template use Options | File Templates. + */ +public class TypeEvaluator { + private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.typeMigration.TypeEvaluator"); + + private final HashMap> myTypeMap; + private final TypeMigrationRules myRules; + private final TypeMigrationLabeler myLabeler; + + + public TypeEvaluator(final LinkedList> types, final TypeMigrationLabeler labeler) { + myLabeler = labeler; + myRules = labeler.getRules(); + myTypeMap = new HashMap>(); + + for (final Pair p : types) { + final LinkedList e = new LinkedList(); + + e.addFirst(p.getSecond()); + + myTypeMap.put(p.getFirst(), e); + } + + } + + public boolean setType(final TypeMigrationUsageInfo usageInfo, @NotNull PsiType type) { + final LinkedList t = myTypeMap.get(usageInfo); + + final PsiElement element = usageInfo.getElement(); + + if (type instanceof PsiEllipsisType && !(element instanceof PsiParameter && ((PsiParameter)element).getDeclarationScope() instanceof PsiMethod)) { + type = ((PsiEllipsisType)type).toArrayType(); + } + + if (t != null) { + if (!t.getFirst().equals(type)) { + if (element instanceof PsiVariable || element instanceof PsiMethod) { + return false; + } + + t.addFirst(type); + + return true; + } + } + else { + final LinkedList e = new LinkedList(); + + e.addFirst(type); + + myTypeMap.put(usageInfo, e); + return true; + } + + return false; + } + + @Nullable + public PsiType getType(PsiElement element) { + for (Map.Entry> entry : myTypeMap.entrySet()) { + if (Comparing.equal(element, entry.getKey().getElement())) { + return entry.getValue().getFirst(); + } + } + if (element.getTextRange() == null) return null; + return getType(new TypeMigrationUsageInfo(element)); + } + + @Nullable + public PsiType getType(final TypeMigrationUsageInfo usageInfo) { + final LinkedList e = myTypeMap.get(usageInfo); + + if (e != null) { + return e.getFirst(); + } + + return TypeMigrationLabeler.getElementType(usageInfo.getElement()); + } + + @Nullable + public PsiType evaluateType(final PsiExpression expr) { + if (expr == null) return null; + final LinkedList e = myTypeMap.get(new TypeMigrationUsageInfo(expr)); + + if (e != null) { + return e.getFirst(); + } + + if (expr instanceof PsiArrayAccessExpression) { + final PsiType at = evaluateType(((PsiArrayAccessExpression)expr).getArrayExpression()); + + if (at instanceof PsiArrayType) { + return ((PsiArrayType)at).getComponentType(); + } + } + else if (expr instanceof PsiAssignmentExpression) { + return evaluateType(((PsiAssignmentExpression)expr).getLExpression()); + } + else if (expr instanceof PsiMethodCallExpression) { + final PsiMethodCallExpression call = (PsiMethodCallExpression)expr; + final JavaResolveResult resolveResult = call.resolveMethodGenerics(); + final PsiMethod method = (PsiMethod)resolveResult.getElement(); + + if (method != null) { + final PsiParameter[] parameters = method.getParameterList().getParameters(); + final PsiExpression[] actualParms = call.getArgumentList().getExpressions(); + return PsiImplUtil.normalizeWildcardTypeByPosition(createMethodSubstitution(parameters, actualParms, method, call, resolveResult.getSubstitutor(), false).substitute(evaluateType(call.getMethodExpression())), expr); + } + } + else if (expr instanceof PsiBinaryExpression) { + final PsiExpression lOperand = ((PsiBinaryExpression)expr).getLOperand(); + final PsiExpression rOperand = ((PsiBinaryExpression)expr).getROperand(); + final PsiJavaToken operationSign = ((PsiBinaryExpression)expr).getOperationSign(); + return PsiBinaryExpressionImpl.calcTypeForBinaryExpression(evaluateType(lOperand), evaluateType(rOperand), operationSign.getTokenType(), + true); + } + else if (expr instanceof PsiPostfixExpression) { + return evaluateType(((PsiPostfixExpression)expr).getOperand()); + } + else if (expr instanceof PsiPrefixExpression) { + return evaluateType(((PsiPrefixExpression)expr).getOperand()); + } + else if (expr instanceof PsiParenthesizedExpression) { + return evaluateType(((PsiParenthesizedExpression)expr).getExpression()); + } + else if (expr instanceof PsiConditionalExpression) { + final PsiExpression thenExpression = ((PsiConditionalExpression)expr).getThenExpression(); + final PsiExpression elseExpression = ((PsiConditionalExpression)expr).getElseExpression(); + + final PsiType thenType = evaluateType(thenExpression); + final PsiType elseType = evaluateType(elseExpression); + + switch ((thenType == null ? 0 : 1) + (elseType == null ? 0 : 2)) { + case 0: + return expr.getType(); + + case 1: + return thenType; + + case 2: + return elseType; + + case 3: + if (TypeConversionUtil.areTypesConvertible(thenType, elseType)) { + return thenType; + } + else if (TypeConversionUtil.areTypesConvertible(elseType, thenType)) { + return elseType; + } else { + switch ((thenType.equals(thenExpression.getType()) ? 0 : 1) + (elseType.equals(elseExpression.getType()) ? 0 : 2)) { + case 0: + return expr.getType(); + + case 1: + return thenType; + + case 2: + return elseType; + + case 3: + LOG.error("Condition type conflict."); + return null; + + default: + LOG.error("Must not happen."); + return null; + } + } + + default: + LOG.error("Must not happen."); + } + + } + else if (expr instanceof PsiNewExpression) { + final PsiExpression qualifier = ((PsiNewExpression)expr).getQualifier(); + + if (qualifier != null) { + final PsiClassType.ClassResolveResult qualifierResult = resolveType(evaluateType(qualifier)); + + if (qualifierResult.getElement() != null) { + final PsiSubstitutor qualifierSubs = qualifierResult.getSubstitutor(); + final PsiClassType.ClassResolveResult result = resolveType(expr.getType()); + + if (result.getElement() != null) { + final PsiClass aClass = result.getElement(); + + return JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory() + .createType(aClass, result.getSubstitutor().putAll(qualifierSubs)); + } + } + } + } + else if (expr instanceof PsiReferenceExpression) { + final PsiType type = evaluateReferenceExpressionType(expr); + if (type != null) { + return PsiImplUtil.normalizeWildcardTypeByPosition(type, expr); + } + } else if (expr instanceof PsiSuperExpression) { + final PsiClass psiClass = PsiTreeUtil.getParentOfType(expr, PsiClass.class); + if (psiClass != null) { + final PsiClass superClass = psiClass.getSuperClass(); + if (superClass != null) { + return getType(new TypeMigrationUsageInfo(superClass)); + } + } + } + + return getType(expr); + } + + @Nullable + private PsiType evaluateReferenceExpressionType(PsiExpression expr) { + final PsiReferenceExpression ref = (PsiReferenceExpression)expr; + final PsiExpression qualifier = ref.getQualifierExpression(); + + if (qualifier == null) { + final PsiElement resolvee = ref.resolve(); + + if (resolvee == null) { + return null; + } + + return resolvee instanceof PsiClass ? JavaPsiFacade.getElementFactory(resolvee.getProject()).createType((PsiClass)resolvee, PsiSubstitutor.EMPTY) : getType(resolvee); + } + else { + final PsiType qualifierType = evaluateType(qualifier); + if (!(qualifierType instanceof PsiArrayType)) { + final PsiElement element = ref.resolve(); + + final PsiClassType.ClassResolveResult result = resolveType(qualifierType); + + final PsiClass aClass = result.getElement(); + if (aClass != null) { + final PsiSubstitutor aSubst = result.getSubstitutor(); + if (element instanceof PsiField) { + final PsiField field = (PsiField)element; + PsiType aType = field.getType(); + final PsiClass superClass = field.getContainingClass(); + if (InheritanceUtil.isInheritorOrSelf(aClass, superClass, true)) { + aType = TypeConversionUtil.getSuperClassSubstitutor(superClass, aClass, PsiSubstitutor.EMPTY).substitute(aType); + } + return aSubst.substitute(aType); + } else if (element instanceof PsiMethod){ + final PsiMethod method = (PsiMethod)element; + PsiType aType = method.getReturnType(); + final PsiClass superClass = method.getContainingClass(); + if (InheritanceUtil.isInheritorOrSelf(aClass, superClass, true)) { + aType = TypeConversionUtil.getSuperClassSubstitutor(superClass, aClass, PsiSubstitutor.EMPTY).substitute(aType); + } else if (InheritanceUtil.isInheritorOrSelf(superClass, aClass, true)) { + final PsiMethod[] methods = method.findSuperMethods(aClass); + if (methods.length > 0) { + aType = methods[0].getReturnType(); + } + /*} + final Pair pair = myRules.bindTypeParameters(qualifier.getType(), qualifierType, method, ref, myLabeler); + if (pair != null) { + final PsiClass psiClass = PsiUtil.resolveClassInType(aType); + if (psiClass instanceof PsiTypeParameter) { + aSubst = aSubst.put((PsiTypeParameter)psiClass, pair.getSecond()); + }*/ + } + return aSubst.substitute(aType); + } + } + } + } + return null; + } + + public static PsiClassType.ClassResolveResult resolveType(PsiType type) { + final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(type); + final PsiClass aClass = resolveResult.getElement(); + if (aClass instanceof PsiAnonymousClass) { + final PsiClassType baseClassType = ((PsiAnonymousClass)aClass).getBaseClassType(); + return resolveType(resolveResult.getSubstitutor().substitute(baseClassType)); + } + return resolveResult; + } + + public PsiSubstitutor createMethodSubstitution(final PsiParameter[] parameters, final PsiExpression[] actualParms, final PsiMethod method, + final PsiExpression call) { + return createMethodSubstitution(parameters, actualParms, method, call, PsiSubstitutor.EMPTY, false); + } + + public PsiSubstitutor createMethodSubstitution(final PsiParameter[] parameters, + final PsiExpression[] actualParms, + final PsiMethod method, + final PsiExpression call, + PsiSubstitutor subst, + boolean preferSubst) { + final SubstitutorBuilder substitutorBuilder = new SubstitutorBuilder(method, call, subst); + + for (int i = 0; i < Math.min(parameters.length, actualParms.length); i++) { + substitutorBuilder.bindTypeParameters(getType(parameters[i]), evaluateType(actualParms[i])); + } + return substitutorBuilder.createSubstitutor(preferSubst); + } + + public String getReport() { + final StringBuffer buffer = new StringBuffer(); + + final String[] t = new String[myTypeMap.size()]; + int k = 0; + + for (final TypeMigrationUsageInfo info : myTypeMap.keySet()) { + final LinkedList types = myTypeMap.get(info); + final StringBuffer b = new StringBuffer(); + + if (types != null) { + b.append(info.getElement()).append(" : "); + + b.append(StringUtil.join(types, new Function() { + public String fun(final PsiType psiType) { + return psiType.getCanonicalText(); + } + }, " ")); + + b.append("\n"); + } + + t[k++] = b.toString(); + } + + Arrays.sort(t); + + for (String aT : t) { + buffer.append(aT); + } + + return buffer.toString(); + } + + public LinkedList> getMigratedDeclarations() { + final LinkedList> list = new LinkedList>(); + + for (final TypeMigrationUsageInfo usageInfo : myTypeMap.keySet()) { + final LinkedList types = myTypeMap.get(usageInfo); + final PsiElement element = usageInfo.getElement(); + if (element instanceof PsiVariable || element instanceof PsiMethod) { + list.addLast(new Pair(usageInfo, types.getFirst())); + } + } + + return list; + } + + @Nullable + static PsiType substituteType(final PsiType migrationType, final PsiType originalType, boolean captureWildcard, PsiClass originalClass, final PsiType rawTypeToReplace) { + if (originalClass != null) { + if (((PsiClassType)originalType).hasParameters() && ((PsiClassType)migrationType).hasParameters()) { + final PsiResolveHelper psiResolveHelper = JavaPsiFacade.getInstance(originalClass.getProject()).getResolveHelper(); + + final PsiType rawOriginalType = JavaPsiFacade.getElementFactory(originalClass.getProject()).createType(originalClass, PsiSubstitutor.EMPTY); + + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; + for (PsiTypeParameter parameter : originalClass.getTypeParameters()) { + final PsiType type = psiResolveHelper.getSubstitutionForTypeParameter(parameter, rawOriginalType, migrationType, false, PsiUtil.getLanguageLevel(originalClass)); + if (type != null) { + substitutor = substitutor.put(parameter, captureWildcard && type instanceof PsiWildcardType ? ((PsiWildcardType)type).getExtendsBound() : type); + } else { + return null; + } + } + + return substitutor.substitute(rawTypeToReplace); + } else { + return originalType; + } + } + return null; + } + + public static PsiType substituteType(final PsiType migrationTtype, final PsiType originalType, final boolean isContraVariantPosition) { + if ( originalType instanceof PsiClassType && migrationTtype instanceof PsiClassType) { + final PsiClass originalClass = ((PsiClassType)originalType).resolve(); + if (isContraVariantPosition && ((PsiClassType)originalType).rawType().isAssignableFrom(((PsiClassType)migrationTtype).rawType())) { + final PsiClass psiClass = ((PsiClassType)migrationTtype).resolve(); + final PsiSubstitutor substitutor = TypeConversionUtil.getClassSubstitutor(originalClass, psiClass, PsiSubstitutor.EMPTY); + if (substitutor != null) { + final PsiType psiType = + substituteType(migrationTtype, originalType, false, psiClass, JavaPsiFacade.getElementFactory(psiClass.getProject()).createType(originalClass, substitutor)); + if (psiType != null) { + return psiType; + } + } + } + else if (!isContraVariantPosition && ((PsiClassType)migrationTtype).rawType().isAssignableFrom(((PsiClassType)originalType).rawType())) { + final PsiType psiType = substituteType(migrationTtype, originalType, false, originalClass, JavaPsiFacade.getElementFactory(originalClass.getProject()).createType(originalClass, PsiSubstitutor.EMPTY)); + if (psiType != null) { + return psiType; + } + } + } + return migrationTtype; + } + + private class SubstitutorBuilder { + private final Map myMapping; + private final PsiMethod myMethod; + private final PsiExpression myCall; + private final PsiSubstitutor mySubst; + + + public SubstitutorBuilder(PsiMethod method, PsiExpression call, PsiSubstitutor subst) { + mySubst = subst; + myMapping = new HashMap(); + myMethod = method; + myCall = call; + } + + private void update(final PsiTypeParameter p, PsiType t) { + if (t instanceof PsiPrimitiveType) { + t = ((PsiPrimitiveType)t).getBoxedType(myMethod); + } + final PsiType binding = myMapping.get(p); + + if (binding == null) { + myMapping.put(p, t); + } + else if (t != null) { + myMapping.put(p, PsiIntersectionType.createIntersection(binding, t)); + } + } + + void bindTypeParameters(PsiType formal, final PsiType actual) { + if (formal instanceof PsiWildcardType) formal = ((PsiWildcardType)formal).getBound(); + + if (formal instanceof PsiArrayType && actual instanceof PsiArrayType) { + bindTypeParameters(((PsiArrayType)formal).getComponentType(), ((PsiArrayType)actual).getComponentType()); + return; + } + + final Pair typePair = myRules.bindTypeParameters(formal, actual, myMethod, myCall, myLabeler); + if (typePair != null) { + bindTypeParameters(typePair.getFirst(), typePair.getSecond()); + return; + } + + final PsiClassType.ClassResolveResult resultF = resolveType(formal); + final PsiClass classF = resultF.getElement(); + if (classF != null) { + + if (classF instanceof PsiTypeParameter) { + update((PsiTypeParameter)classF, actual); + return; + } + + final PsiClassType.ClassResolveResult resultA = resolveType(actual); + + final PsiClass classA = resultA.getElement(); + if (classA == null) { + return; + } + + + if (!classA.equals(classF)) { + final PsiSubstitutor superClassSubstitutor = + TypeConversionUtil.getClassSubstitutor(classF, classA, resultA.getSubstitutor()); + if (superClassSubstitutor != null) { + final PsiType aligned = JavaPsiFacade.getInstance(classF.getProject()).getElementFactory().createType(classF, superClassSubstitutor); + bindTypeParameters(formal, aligned); + } + } + + final PsiTypeParameter[] typeParms = classA.getTypeParameters(); + final PsiSubstitutor substA = resultA.getSubstitutor(); + final PsiSubstitutor substF = resultF.getSubstitutor(); + + for (PsiTypeParameter typeParm : typeParms) { + bindTypeParameters(substF.substitute(typeParm), substA.substitute(typeParm)); + } + } + } + + public PsiSubstitutor createSubstitutor(boolean preferSubst) { + PsiSubstitutor theSubst = mySubst; + if (preferSubst) { + myMapping.keySet().removeAll(mySubst.getSubstitutionMap().keySet()); + } + for (final PsiTypeParameter parm : myMapping.keySet()) { + theSubst = theSubst.put(parm, myMapping.get(parm)); + } + return theSubst; + } + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationLabeler.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationLabeler.java new file mode 100644 index 000000000000..c1a88667de4b --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationLabeler.java @@ -0,0 +1,830 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.lang.StdLanguages; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiImplUtil; +import com.intellij.psi.javadoc.PsiDocTagValue; +import com.intellij.psi.search.PsiSearchScopeUtil; +import com.intellij.psi.search.SearchScope; +import com.intellij.psi.search.searches.OverridingMethodsSearch; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.usageInfo.OverridenUsageInfo; +import com.intellij.refactoring.typeMigration.usageInfo.OverriderUsageInfo; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.Query; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.*; + +/** + * Created by IntelliJ IDEA. + * User: db + * Date: Sep 19, 2004 + * Time: 6:13:23 PM + * To change this template use File | Settings | File Templates. + */ +public class TypeMigrationLabeler { + private static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.typeMigration.TypeMigrationLabeler"); + private boolean myShowWarning = true; + + public TypeMigrationRules getRules() { + return myRules; + } + + private final TypeMigrationRules myRules; + private TypeEvaluator myTypeEvaluator; + private final LinkedHashMap myConversions; + private final HashSet> myFailedConversions; + private LinkedList> myMigrationRoots; + private final LinkedHashMap myNewExpressionTypeChange; + private final LinkedHashMap myClassTypeArgumentsChange; + + private TypeMigrationUsageInfo[] myMigratedUsages = null; + + private TypeMigrationUsageInfo myCurrentRoot; + private final Map>> myRootsTree = + new HashMap>>(); + private final Map, Set> myRootUsagesTree = new HashMap, Set>(); + private final Set myProcessedRoots = new HashSet(); + + + public TypeMigrationLabeler(final TypeMigrationRules rules) { + myRules = rules; + + myConversions = new LinkedHashMap(); + myFailedConversions = new HashSet>(); + myNewExpressionTypeChange = new LinkedHashMap(); + myClassTypeArgumentsChange = new LinkedHashMap(); + } + + public boolean hasFailedConversions() { + return myFailedConversions.size() > 0; + } + + public String[] getFailedConversionsReport() { + final String[] report = new String[myFailedConversions.size()]; + int j = 0; + + for (final Pair p : myFailedConversions) { + final PsiElement element = p.getFirst().retrieve(); + + report[j++] = "Cannot convert type of expression " + + StringUtil.escapeXml(element.getText()) + + "" + + " from " + + StringUtil.escapeXml(((PsiExpression)element).getType().getCanonicalText()) + + " to " + StringUtil.escapeXml(p.getSecond().getCanonicalText()) + + "
"; + } + + return report; + } + + public UsageInfo[] getFailedUsages() { + final List usages = new ArrayList(myFailedConversions.size()); + for (final Pair p : myFailedConversions) { + final PsiExpression expr = (PsiExpression)p.getFirst().retrieve(); + if (expr != null) { + usages.add(new UsageInfo(expr) { + public String getTooltipText() { + return "Cannot convert type of the expression from " + + expr.getType().getCanonicalText() + + " to " + + p.getSecond().getCanonicalText(); + } + }); + } + } + + return usages.toArray(new UsageInfo[usages.size()]); + } + + public TypeMigrationUsageInfo[] getMigratedUsages() { + final LinkedList> declarations = getTypeEvaluator().getMigratedDeclarations(); + final TypeMigrationUsageInfo[] usages = new TypeMigrationUsageInfo[declarations.size() + myConversions.size() + myNewExpressionTypeChange.size() + myClassTypeArgumentsChange.size()]; + + int j = 0; + + List conversionExprs = new ArrayList(myConversions.keySet()); + Collections.sort(conversionExprs, new Comparator() { + public int compare(final PsiElement e1, final PsiElement e2) { + return e2.getTextRange().getStartOffset() - e1.getTextRange().getStartOffset(); + } + }); + for (final PsiElement element : conversionExprs) { + final Object conv = myConversions.get(element); + + usages[j++] = new TypeMigrationUsageInfo(element) { + public String getTooltipText() { + if (conv instanceof String) { //todo + final String conversion = (String)conv; + return "Replaced with " + conversion.replaceAll("\\$", element.getText()); + } + else { + return "Replaced with " + conv.toString(); + } + } + + @Override + public boolean isExcluded() { + if (conv instanceof TypeConversionDescriptorBase) return ((TypeConversionDescriptorBase)conv).getRoot().isExcluded(); + return super.isExcluded(); + } + }; + } + + for (final TypeMigrationUsageInfo expr : myNewExpressionTypeChange.keySet()) { + + usages[j++] = expr; + } + + for (final Pair p : declarations) { + final TypeMigrationUsageInfo element = p.getFirst(); + usages[j++] = element; + } + + for (TypeMigrationUsageInfo info : myClassTypeArgumentsChange.keySet()) { + usages[j++] = info; + } + return usages; + } + + public void change(final TypeMigrationUsageInfo usageInfo) { + final PsiElement element = usageInfo.getElement(); + if (element == null) return; + final Project project = element.getProject(); + if (element instanceof PsiExpression) { + final PsiExpression expression = (PsiExpression)element; + if (element instanceof PsiNewExpression) { + for (Map.Entry info : myNewExpressionTypeChange.entrySet()) { + final PsiElement expressionToReplace = info.getKey().getElement(); + if (expression.equals(expressionToReplace)) { + TypeMigrationReplacementUtil.replaceNewExpressionType(project, (PsiNewExpression)expressionToReplace, info); + } + } + } + final Object conversion = myConversions.get(element); + if (conversion != null) { + myConversions.remove(element); + TypeMigrationReplacementUtil.replaceExpression(expression, project, conversion); + } + } else if (element instanceof PsiReferenceParameterList) { + for (Map.Entry entry : myClassTypeArgumentsChange.entrySet()) { + if (element.equals(entry.getKey().getElement())) { //todo check null + final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); + try { + element.getParent().replace(factory.createReferenceElementByType(entry.getValue())); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + } + } + else { + TypeMigrationReplacementUtil.migratePsiMemeberType(element, project, getTypeEvaluator().getType(usageInfo)); + } + } + + @Nullable + Object getConversion(PsiElement element) { + return myConversions.get(element); + } + + public TypeMigrationUsageInfo[] getMigratedUsages(boolean autoMigrate, final PsiElement... roots) { + if (myMigratedUsages == null) { + myShowWarning = autoMigrate; + migrate(autoMigrate, roots); + myMigratedUsages = getMigratedUsages(); + } + return myMigratedUsages; + } + + @Nullable + public Set getTypeUsages(final TypeMigrationUsageInfo element, final TypeMigrationUsageInfo currentRoot) { + return myRootUsagesTree.get(Pair.create(element, currentRoot)); + } + + void convertExpression(final PsiExpression expr, final PsiType toType, final PsiType fromType, final boolean isCovariantPosition) { + final TypeConversionDescriptorBase conversion = myRules.findConversion(fromType, toType, expr instanceof PsiMethodCallExpression ? ((PsiMethodCallExpression)expr).resolveMethod() : null, expr, + isCovariantPosition, this); + + if (conversion == null) { + markFailedConversion(new Pair(fromType, toType), expr); + } + else { + setConversionMapping(expr, conversion); + } + } + + public void migrateExpressionType(final PsiExpression expr, final PsiType migrationType, final PsiElement place, boolean alreadyProcessed, final boolean isCovariant) { + PsiType originalType = expr.getType(); + + LOG.assertTrue(originalType != null); + + if (originalType.equals(migrationType)) return; + + if (originalType.equals(PsiType.NULL)) { + if (migrationType instanceof PsiPrimitiveType) { + markFailedConversion(new Pair(originalType, migrationType), expr); + } + return; + } + + if (expr instanceof PsiConditionalExpression) { + + } else if (expr instanceof PsiClassObjectAccessExpression) { + if (!TypeConversionUtil.isAssignable(migrationType, expr.getType())) { + markFailedConversion(new Pair(expr.getType(), migrationType), expr); + return; + } + } else if (expr instanceof PsiArrayInitializerExpression && migrationType instanceof PsiArrayType) { + final PsiExpression[] initializers = ((PsiArrayInitializerExpression)expr).getInitializers(); + for (PsiExpression initializer : initializers) { + migrateExpressionType(initializer, ((PsiArrayType)migrationType).getComponentType(), expr, alreadyProcessed, true); + } + getTypeEvaluator().setType(new TypeMigrationUsageInfo(expr), migrationType); + return; + } else if (expr instanceof PsiArrayAccessExpression) { + migrateExpressionType(((PsiArrayAccessExpression)expr).getArrayExpression(), migrationType.createArrayType(), place, alreadyProcessed, isCovariant); + return; + } + else if (expr instanceof PsiReferenceExpression) { + final PsiElement resolved = ((PsiReferenceExpression)expr).resolve(); + if (resolved != null) { + if (!addMigrationRoot(resolved, migrationType, place, alreadyProcessed, !isCovariant)) { + convertExpression(expr, migrationType, getTypeEvaluator().evaluateType(expr), isCovariant); + } + } + return; + } + else if (expr instanceof PsiMethodCallExpression) { + final PsiMethod resolved = ((PsiMethodCallExpression)expr).resolveMethod(); + if (resolved != null) { + if (!addMigrationRoot(resolved, migrationType, place, alreadyProcessed, !isCovariant)) { + convertExpression(expr, migrationType, getTypeEvaluator().evaluateType(expr), isCovariant); + } + } + return; + } + else if (expr instanceof PsiNewExpression) { + if (originalType.getArrayDimensions() == migrationType.getArrayDimensions()) { + if (migrationType.getArrayDimensions() > 0) { + final PsiType elemenType = ((PsiArrayType)migrationType).getComponentType(); + + final PsiArrayInitializerExpression arrayInitializer = ((PsiNewExpression)expr).getArrayInitializer(); + + if (arrayInitializer != null) { + final PsiExpression[] initializers = arrayInitializer.getInitializers(); + for (int i = initializers.length - 1; i >= 0; i--) { + migrateExpressionType(initializers[i], elemenType, place, alreadyProcessed, true); + } + } + + if (isGenericsArrayType(elemenType)){ + markFailedConversion(new Pair(originalType, migrationType), expr); + return; + } + + myNewExpressionTypeChange.put(new TypeMigrationUsageInfo(expr), migrationType); + getTypeEvaluator().setType(new TypeMigrationUsageInfo(expr), migrationType); + return; + } else { + if (migrationType instanceof PsiClassType && originalType instanceof PsiClassType && ((PsiClassType)migrationType).rawType().isAssignableFrom(((PsiClassType)originalType).rawType())) { + final PsiClass originalClass = PsiUtil.resolveClassInType(originalType); + if (originalClass instanceof PsiAnonymousClass) { + originalType = ((PsiAnonymousClass)originalClass).getBaseClassType(); + } + final PsiType type = TypeEvaluator.substituteType(migrationType, originalType, true, ((PsiClassType)originalType).resolveGenerics().getElement(), + JavaPsiFacade.getElementFactory(expr.getProject()).createType(((PsiClassType)originalType).resolve(), PsiSubstitutor.EMPTY)); + if (type != null){ + myNewExpressionTypeChange.put(new TypeMigrationUsageInfo(expr), type); + getTypeEvaluator().setType(new TypeMigrationUsageInfo(expr), type); + return; + } + } + } + } + + } + + convertExpression(expr, migrationType, originalType, isCovariant); + } + + private static boolean isGenericsArrayType(final PsiType elemenType) { + if (elemenType instanceof PsiClassType && ((PsiClassType)elemenType).hasParameters()) { + return true; + } else if (elemenType instanceof PsiArrayType) { + final PsiType componentType = ((PsiArrayType)elemenType).getComponentType(); + return isGenericsArrayType(componentType); + } + return false; + } + + boolean addMigrationRoot(PsiElement element, PsiType type, final PsiElement place, boolean alreadyProcessed, final boolean isContraVariantPosition) { + return addMigrationRoot(element, type, place, alreadyProcessed, isContraVariantPosition, false); + } + + boolean addMigrationRoot(PsiElement element, + PsiType type, + final PsiElement place, + boolean alreadyProcessed, + final boolean isContraVariantPosition, + final boolean userDefinedType) { + if (type.equals(PsiType.NULL)) { + return false; + } + + final PsiElement resolved = Util.normalizeElement(element); + + final SearchScope searchScope = myRules.getSearchScope(); + if (!resolved.isPhysical() || !PsiSearchScopeUtil.isInScope(searchScope, resolved)) { + return false; + } + + final PsiType originalType = getElementType(resolved); + + LOG.assertTrue(originalType != null); + + type = userDefinedType ? type : TypeEvaluator.substituteType(type, originalType, isContraVariantPosition); + + if (!userDefinedType) { + if (originalType instanceof PsiClassType) { + final PsiClassType psiClassType = (PsiClassType)originalType; + if (psiClassType.resolve() instanceof PsiTypeParameter) { + return false; + } + for (PsiType paramType : psiClassType.getParameters()) { + if (paramType instanceof PsiClassType && ((PsiClassType)paramType).resolve() instanceof PsiTypeParameter) return false; + } + } + } + + if (type instanceof PsiCapturedWildcardType) { + return false; + } + + if (resolved instanceof PsiMethod) { + final PsiMethod method = ((PsiMethod)resolved); + final PsiMethod[] methods = OverridingMethodsSearch.search(method, method.getUseScope(), false).toArray(PsiMethod.EMPTY_ARRAY); + final OverridenUsageInfo overridenUsageInfo = new OverridenUsageInfo(method); + final OverriderUsageInfo[] overriders = new OverriderUsageInfo[methods.length]; + for (int i = -1; i < methods.length; i++) { + final TypeMigrationUsageInfo m; + if (i < 0) { + m = overridenUsageInfo; + } + else { + overriders[i] = new OverriderUsageInfo(methods[i], method); + m = overriders[i]; + } + + alreadyProcessed = addRoot(m, type, place, alreadyProcessed); + } + overridenUsageInfo.setOverriders(overriders); + + return !alreadyProcessed; + } + else if (resolved instanceof PsiParameter && ((PsiParameter)resolved).getDeclarationScope() instanceof PsiMethod) { + final PsiMethod method = PsiTreeUtil.getParentOfType(resolved, PsiMethod.class); + + if (method == null) { + return false; + } + + final int index = method.getParameterList().getParameterIndex(((PsiParameter)resolved)); + final PsiMethod[] methods = OverridingMethodsSearch.search(method, method.getUseScope(), false).toArray(PsiMethod.EMPTY_ARRAY); + + final OverriderUsageInfo[] overriders = new OverriderUsageInfo[methods.length]; + final OverridenUsageInfo overridenUsageInfo = new OverridenUsageInfo(method.getParameterList().getParameters()[index]); + for (int i = -1; i < methods.length; i++) { + final PsiMethod m = i < 0 ? method : methods[i]; + final PsiParameter p = m.getParameterList().getParameters()[index]; + final TypeMigrationUsageInfo paramUsageInfo; + if (i < 0) { + paramUsageInfo = overridenUsageInfo; + } + else { + overriders[i] = new OverriderUsageInfo(p, method); + paramUsageInfo = overriders[i]; + } + alreadyProcessed = addRoot(paramUsageInfo, type, place, alreadyProcessed); + } + + overridenUsageInfo.setOverriders(overriders); + + return !alreadyProcessed; + } + else { + return !addRoot(new TypeMigrationUsageInfo(resolved), type, place, alreadyProcessed); + } + } + + + + @Nullable + public static PsiType getElementType(final PsiElement resolved) { + if (resolved instanceof PsiVariable) { + return ((PsiVariable)resolved).getType(); + } + else { + if (resolved instanceof PsiMethod) { + return (((PsiMethod)resolved).getReturnType()); + } + else if (resolved instanceof PsiExpression){ + return (((PsiExpression)resolved).getType()); + } else if (resolved instanceof PsiReferenceParameterList) { + final PsiElement parent = resolved.getParent(); + LOG.assertTrue(parent instanceof PsiJavaCodeReferenceElement); + final PsiClass psiClass = (PsiClass)((PsiJavaCodeReferenceElement)parent).resolve(); + return JavaPsiFacade.getElementFactory(parent.getProject()).createType(psiClass, TypeConversionUtil.getSuperClassSubstitutor(psiClass, PsiTreeUtil.getParentOfType(parent, + PsiClass.class), + PsiSubstitutor.EMPTY)); + } else if (resolved instanceof PsiClass) { + return JavaPsiFacade.getElementFactory(resolved.getProject()).createType((PsiClass)resolved, PsiSubstitutor.EMPTY); + } + } + LOG.error("should not happen: " + resolved.getClass()); + return null; + } + + boolean addRoot(final TypeMigrationUsageInfo usageInfo, final PsiType type, final PsiElement place, boolean alreadyProcessed) { + if (myShowWarning && myMigrationRoots.size() > 10 && !ApplicationManager.getApplication().isUnitTestMode()) { + final MigrateException[] ex = new MigrateException[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + if (Messages.showYesNoCancelDialog("Found more than 10 roots to migrate. Do you want to preview?", "Type Migration", Messages.getWarningIcon()) == DialogWrapper.OK_EXIT_CODE) { + ex[0] = new MigrateException(); + } + } + }); + } + catch (Exception e) { + //do nothing + } + if (ex[0] != null) throw ex[0]; + myShowWarning = false; + } + rememberRootTrace(usageInfo, type, place, alreadyProcessed); + if (!alreadyProcessed && !getTypeEvaluator().setType(usageInfo, type)) { + alreadyProcessed = true; + } + + if (!alreadyProcessed) myMigrationRoots.addFirst(new Pair(usageInfo, type)); + return alreadyProcessed; + } + + private void rememberRootTrace(final TypeMigrationUsageInfo usageInfo, final PsiType type, final PsiElement place, final boolean alreadyProcessed) { + if (myCurrentRoot != null) { + if (!alreadyProcessed) { + myProcessedRoots.add(usageInfo); + } + + if (myProcessedRoots.contains(usageInfo)) { + HashSet> infos = myRootsTree.get(myCurrentRoot); + if (infos == null) { + infos = new HashSet>(); + myRootsTree.put(myCurrentRoot, infos); + } + infos.add(Pair.create(usageInfo, type)); + } + if (!(usageInfo instanceof OverriderUsageInfo)) { //hide the same usage for all overriders + setTypeUsage(usageInfo, place); + } + } + } + + private void setTypeUsage(final TypeMigrationUsageInfo usageInfo, final PsiElement place) { + if (place != null) { + final Pair rooted = Pair.create(usageInfo, myCurrentRoot); + Set usages = myRootUsagesTree.get(rooted); + if (usages == null) { + usages = new HashSet(); + myRootUsagesTree.put(rooted, usages); + } + usages.add(place); + } + } + + public void setTypeUsage(final PsiElement element, final PsiElement place) { + setTypeUsage(new TypeMigrationUsageInfo(element), place); + } + + void markFailedConversion(final Pair typePair, final PsiExpression expression) { + myFailedConversions.add(new Pair(PsiAnchor.create(expression), typePair.getSecond())); + } + + void setConversionMapping(final PsiExpression expression, final Object obj) { + if (myConversions.get(expression) != null) { + return; + } + + if (obj instanceof TypeConversionDescriptorBase) { + ((TypeConversionDescriptorBase)obj).setRoot(myCurrentRoot); + } + myConversions.put(expression, obj); + } + + public PsiReference[] markRootUsages(final PsiElement element, final PsiType migrationType) { + return markRootUsages(element, migrationType, ReferencesSearch.search(element, element.getUseScope(), false).toArray(new PsiReference[0])); + } + + PsiReference[] markRootUsages(final PsiElement element, final PsiType migrationType, final PsiReference[] refs) { + final List validReferences = new ArrayList(); + for (PsiReference ref1 : refs) { + final PsiElement ref = ref1.getElement(); + + if (ref != null) { + if (element instanceof PsiMethod) { + final PsiElement parent = Util.getEssentialParent(ref); + + if (!(parent instanceof PsiMethodCallExpression)) { + continue; + } + + getTypeEvaluator().setType(new TypeMigrationUsageInfo(parent), migrationType); + } + else if (element instanceof PsiVariable) { + if (ref instanceof PsiReferenceExpression) { + getTypeEvaluator().setType(new TypeMigrationUsageInfo(ref), PsiImplUtil.normalizeWildcardTypeByPosition(migrationType, (PsiReferenceExpression)ref)); + } + } + else { + LOG.error("Method call expression or reference expression expected but found " + element.getClass().getName()); + continue; + } + validReferences.add(ref1); + } + } + + Collections.sort(validReferences, new Comparator() { + public int compare(final PsiReference o1, final PsiReference o2) { + return o1.getElement().getTextOffset() - o2.getElement().getTextOffset(); + } + }); + + return validReferences.toArray(new PsiReference[validReferences.size()]); + } + + public void migrateRoot(final PsiElement root, final PsiType migrationType, final PsiReference[] usages) { + if (root instanceof PsiMethod) { + migrateMethodReturnExpression(migrationType, (PsiMethod)root); + } + else if (root instanceof PsiParameter && ((PsiParameter)root).getDeclarationScope() instanceof PsiMethod) { + migrateMethodCallExpressions(migrationType, (PsiParameter)root, null); + } + else if (root instanceof PsiVariable || root instanceof PsiExpression){ + final PsiElement element = getContainingStatement(root); + element.accept(new TypeMigrationStatementProcessor(element, this)); + } else if (root instanceof PsiReferenceParameterList) { + myClassTypeArgumentsChange.put(new TypeMigrationUsageInfo(root), (PsiClassType)migrationType); + new ClassTypeArgumentMigrationProcessor(this).migrateClassTypeParameter((PsiReferenceParameterList)root, migrationType); + } + + final Set processed = new HashSet(); + for (PsiReference usage : usages) { + migrateRootUsageExpression(usage, processed); + } + } + + private static PsiElement getContainingStatement(final PsiElement root) { + final PsiStatement statement = PsiTreeUtil.getParentOfType(root, PsiStatement.class); + final PsiField field = PsiTreeUtil.getParentOfType(root, PsiField.class); + return statement != null ? statement : field != null ? field : root; + } + + void migrateRootUsageExpression(final PsiReference usage, final Set processed) { + final PsiElement ref = usage.getElement(); + if (ref != null && ref.getLanguage() == StdLanguages.JAVA) { + final PsiElement element = getContainingStatement(ref); + if (element != null && !processed.contains(element)) { + processed.add(element); + element.accept(new TypeMigrationStatementProcessor(ref, this)); + } + } + } + + void migrateMethodCallExpressions(final PsiType migrationType, final PsiParameter param, final PsiClass psiClass) { + boolean checkNumberOfArguments = false; + if (param.getType() instanceof PsiEllipsisType && !(migrationType instanceof PsiEllipsisType)) { + checkNumberOfArguments = true; + } + final PsiType strippedType = + migrationType instanceof PsiEllipsisType ? ((PsiEllipsisType)migrationType).getComponentType() : migrationType; + final PsiMethod method = (PsiMethod)param.getDeclarationScope(); + final PsiParameterList parameterList = method.getParameterList(); + final int parametersCount = parameterList.getParametersCount(); + final int index = parameterList.getParameterIndex(param); + final List refs = filterReferences(psiClass, ReferencesSearch.search(method, method.getUseScope().intersectWith(myRules.getSearchScope()), false)); + for (PsiReference ref1 : refs) { + final PsiElement ref = ref1.getElement(); + final PsiElement parent = Util.getEssentialParent(ref); + if (parent instanceof PsiCallExpression) { + final PsiExpressionList argumentList = ((PsiCallExpression)parent).getArgumentList(); + if (argumentList != null) { + final PsiExpression[] expressions = argumentList.getExpressions(); + if (checkNumberOfArguments && parametersCount != expressions.length) { + markFailedConversion(new Pair(param.getType(), migrationType), (PsiCallExpression)parent); + } + if (index > -1 && index < expressions.length) { + for (int idx = index; idx < (param.isVarArgs() ? expressions.length : index + 1); idx++) { + final PsiExpression actual = expressions[idx]; + final PsiType type = getTypeEvaluator().evaluateType(actual); + if (type != null) { + migrateExpressionType(actual, strippedType, parent, TypeConversionUtil.isAssignable(strippedType, type), true); + } + } + } + } + } else if (ref instanceof PsiDocTagValue) { + myConversions.put(ref, method); + } + } + } + + private void migrateMethodReturnExpression(final PsiType migrationType, final PsiMethod method) { + final PsiCodeBlock block = method.getBody(); + if (block != null) { + block.accept(new JavaRecursiveElementWalkingVisitor() { + @Override + public void visitReturnStatement(PsiReturnStatement statement) { + final PsiExpression value = statement.getReturnValue(); + if (value != null) { + final PsiType type = getTypeEvaluator().evaluateType(value); + if (type != null && !type.equals(migrationType)) { + migrateExpressionType(value, migrationType, statement, TypeConversionUtil.isAssignable(migrationType, type), true); + } + } + } + }); + } + } + + private void iterate() { + final LinkedList> roots = + (LinkedList>)myMigrationRoots.clone(); + + myMigrationRoots = new LinkedList>(); + + final PsiReference[][] cachedUsages = new PsiReference[roots.size()][]; + int j = 0; + + for (final Pair p : roots) { + cachedUsages[j++] = markRootUsages(p.getFirst().getElement(), p.getSecond()); + } + + j = 0; + + for (final Pair root : roots) { + myCurrentRoot = root.getFirst(); + migrateRoot(root.getFirst().getElement(), root.getSecond(), cachedUsages[j++]); + } + } + + private void migrate(boolean autoMigrate, final PsiElement... victims) { + myMigrationRoots = new LinkedList>(); + myTypeEvaluator = new TypeEvaluator(myMigrationRoots, this); + + + final PsiType rootType = myRules.getMigrationRootType(); + for (PsiElement victim : victims) { + addMigrationRoot(victim, rootType, null, false, true, true); + } + + if (autoMigrate) { + while (myMigrationRoots.size() > 0) { + iterate(); + } + } + } + + public TypeEvaluator getTypeEvaluator() { + return myTypeEvaluator; + } + + public Map>> getRootsTree() { + return myRootsTree; + } + + public void setCurrentRoot(final TypeMigrationUsageInfo currentRoot) { + myCurrentRoot = currentRoot; + } + + public LinkedList> getMigrationRoots() { + return myMigrationRoots; + } + + public static List filterReferences(final PsiClass psiClass, final Query memberReferences) { + final List refs = new ArrayList(); + for (PsiReference memberReference : memberReferences) { + if (psiClass == null) { + refs.add(memberReference); + } else { + final PsiElement referencedElement = memberReference.getElement(); + if (referencedElement instanceof PsiReferenceExpression) { + final PsiExpression qualifierExpression = ((PsiReferenceExpression)referencedElement).getQualifierExpression(); + if (qualifierExpression != null) { + final PsiType qualifierType = qualifierExpression.getType(); + if (qualifierType instanceof PsiClassType && psiClass == ((PsiClassType)qualifierType).resolve()) { + refs.add(memberReference); + } + } else { + if (psiClass == PsiTreeUtil.getParentOfType(referencedElement, PsiClass.class)) { + refs.add(memberReference); + } + } + } + } + } + return refs; + } + + public String getMigrationReport() { + final StringBuffer buffer = new StringBuffer(); + + buffer.append("Types:\n" + getTypeEvaluator().getReport() + "\n"); + buffer.append("Conversions:\n"); + + final String[] conversions = new String[myConversions.size()]; + int k = 0; + + for (final PsiElement expr : myConversions.keySet()) { + final Object conv = myConversions.get(expr); + + if (conv instanceof Pair && ((Pair)conv).first == null) { + conversions[k++] = (expr.getText() + " -> " + ((Pair)conv).second + "\n"); + } else { + conversions[k++] = (expr.getText() + " -> " + conv + "\n"); + } + } + + Arrays.sort(conversions, new Comparator() { + public int compare(Object x, Object y) { + return ((String)x).compareTo((String)y); + } + }); + + for (String conversion : conversions) { + buffer.append(conversion); + } + + buffer.append("\nNew expression type changes:\n"); + + final String[] newchanges = new String[myNewExpressionTypeChange.size()]; + k = 0; + + for (final Map.Entry entry : myNewExpressionTypeChange.entrySet()) { + + + newchanges[k++] = entry.getKey().getElement().getText() + " -> " + entry.getValue().getCanonicalText() + "\n"; + } + + Arrays.sort(newchanges, new Comparator() { + public int compare(Object x, Object y) { + return ((String)x).compareTo((String)y); + } + }); + + for (String newchange : newchanges) { + buffer.append(newchange); + } + + buffer.append("Fails:\n"); + + final ArrayList> failsList = new ArrayList>(myFailedConversions); + Collections.sort(failsList, new Comparator>() { + public int compare(final Pair o1, final Pair o2) { + final PsiElement element1 = o1.getFirst().retrieve(); + final PsiElement element2 = o2.getFirst().retrieve(); + if (element1 == null || element2 == null) return 0; + return element1.getText().compareTo(element2.getText()); + } + }); + + for (final Pair p : failsList) { + final PsiElement element = p.getFirst().retrieve(); + if (element != null) { + buffer.append(element.getText() + "->" + p.getSecond().getCanonicalText() + "\n"); + } + } + + return buffer.toString(); + } + + public static class MigrateException extends RuntimeException { + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationProcessor.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationProcessor.java new file mode 100644 index 000000000000..6592347b5a2a --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationProcessor.java @@ -0,0 +1,153 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.wm.ToolWindowId; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.psi.*; +import com.intellij.refactoring.BaseRefactoringProcessor; +import com.intellij.refactoring.typeMigration.ui.FailedConversionsDialog; +import com.intellij.refactoring.typeMigration.ui.MigrationPanel; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.ui.content.Content; +import com.intellij.usageView.UsageInfo; +import com.intellij.usageView.UsageViewDescriptor; +import com.intellij.usageView.UsageViewManager; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class TypeMigrationProcessor extends BaseRefactoringProcessor { + + private PsiElement[] myRoot; + private final TypeMigrationRules myRules; + private TypeMigrationLabeler myLabeler; + + public TypeMigrationProcessor(final Project project, final PsiElement root, final TypeMigrationRules rules) { + this(project, new PsiElement[]{root}, rules); + } + + public TypeMigrationProcessor(final Project project, final PsiElement[] roots, final TypeMigrationRules rules) { + super(project); + myRoot = roots; + myRules = rules; + } + + protected UsageViewDescriptor createUsageViewDescriptor(UsageInfo[] usages) { + return new TypeMigrationViewDescriptor(myRoot[0]); + } + + protected boolean preprocessUsages(Ref refUsages) { + if (hasFailedConversions()) { + if (ApplicationManager.getApplication().isUnitTestMode()) { + throw new RuntimeException(StringUtil.join(myLabeler.getFailedConversionsReport(), "\n")); + } + FailedConversionsDialog dialog = new FailedConversionsDialog(myLabeler.getFailedConversionsReport(), myProject); + dialog.show(); + if (!dialog.isOK()) { + final int exitCode = dialog.getExitCode(); + prepareSuccessful(); + if (exitCode == FailedConversionsDialog.VIEW_USAGES_EXIT_CODE) { + previewRefactoring(refUsages.get()); + } + return false; + } + } + prepareSuccessful(); + return true; + } + + public boolean hasFailedConversions() { + return myLabeler.hasFailedConversions(); + } + + @Override + protected void previewRefactoring(final UsageInfo[] usages) { + MigrationPanel panel = new MigrationPanel(myRoot[0], myLabeler, myProject, isPreviewUsages()); + String text; + if (myRoot[0] instanceof PsiField) { + text = "field \'" + ((PsiField)myRoot[0]).getName() + "\'"; + } else if (myRoot[0] instanceof PsiParameter) { + text = "parameter \'" + ((PsiParameter)myRoot[0]).getName() + "\'"; + } else if (myRoot[0] instanceof PsiLocalVariable) { + text = "variable \'" + ((PsiLocalVariable)myRoot[0]).getName() + "\'"; + } else if (myRoot[0] instanceof PsiMethod) { + text = "method \'" + ((PsiMethod)myRoot[0]).getName() + "\' return"; + } else { + text = myRoot.toString(); + } + Content content = UsageViewManager.getInstance(myProject) + .addContent("Migrate Type of " + + text + + " from \'" + + TypeMigrationLabeler.getElementType(myRoot[0]).getPresentableText() + + "\' to \'" + + myRules.getMigrationRootType().getPresentableText() + + "\'", false, panel, true, true); + panel.setContent(content); + ToolWindowManager.getInstance(myProject).getToolWindow(ToolWindowId.FIND).activate(null); + } + + @NotNull + public UsageInfo[] findUsages() { + myLabeler = new TypeMigrationLabeler(myRules); + + try { + return myLabeler.getMigratedUsages(!isPreviewUsages(), myRoot); + } + catch (TypeMigrationLabeler.MigrateException e) { + setPreviewUsages(true); + return myLabeler.getMigratedUsages(false, myRoot); + } + } + + protected void refreshElements(PsiElement[] elements) { + myRoot = elements; + } + + public void performRefactoring(UsageInfo[] usages) { + change(myLabeler, usages); + } + + public static void change(TypeMigrationLabeler labeler, UsageInfo[] usages) { + List nonCodeUsages = new ArrayList(); + for (UsageInfo usage : usages) { + if (((TypeMigrationUsageInfo)usage).isExcluded()) continue; + final PsiElement element = usage.getElement(); + if (element instanceof PsiVariable || element instanceof PsiMember || element instanceof PsiExpression || element instanceof PsiReferenceParameterList) { + labeler.change((TypeMigrationUsageInfo)usage); + } else { + nonCodeUsages.add(usage); + } + } + for (UsageInfo usageInfo : nonCodeUsages) { + final PsiElement element = usageInfo.getElement(); + if (element != null) { + final PsiReference reference = element.getReference(); + if (reference != null) { + final Object target = labeler.getConversion(element); + if (target instanceof PsiMember) { + try { + reference.bindToElement((PsiElement)target); + } + catch (IncorrectOperationException e) { + //skip + } + } + } + } + } + } + + public TypeMigrationLabeler getLabeler() { + return myLabeler; + } + + protected String getCommandName() { + return "TypeMigration"; + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationReplacementUtil.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationReplacementUtil.java new file mode 100644 index 000000000000..a02534b9be8e --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationReplacementUtil.java @@ -0,0 +1,152 @@ +/* + * User: anna + * Date: 04-Apr-2008 + */ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.ChildRole; +import com.intellij.psi.impl.source.tree.CompositeElement; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.util.IncorrectOperationException; + +import java.util.Map; + +public class TypeMigrationReplacementUtil { + public static final Logger LOG = Logger.getInstance("#" + TypeMigrationReplacementUtil.class.getName()); + + private TypeMigrationReplacementUtil() { + } + + public static void replaceExpression(PsiExpression expression, final Project project, Object conversion) { + if (conversion instanceof TypeConversionDescriptorBase) { + ((TypeConversionDescriptorBase)conversion).replace(expression); + } else if (conversion instanceof String) { + String replacement = (String)conversion; + try { + expression.replace( + JavaPsiFacade.getInstance(project).getElementFactory().createExpressionFromText(replacement, expression)); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + else if (expression instanceof PsiReferenceExpression) { + final PsiElement resolved = ((PsiReferenceExpression)expression).resolve(); + final PsiMember replacer = ((PsiMember)conversion); + final String method = ((PsiMember)resolved).getName(); + final String ref = expression.getText(); + final String newref = ref.substring(0, ref.lastIndexOf(method)) + replacer.getName(); + + if (conversion instanceof PsiMethod) { + if (resolved instanceof PsiMethod) { + try { + expression.replace( + JavaPsiFacade.getInstance(project).getElementFactory().createExpressionFromText(newref, expression)); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + else { + try { + expression.replace(JavaPsiFacade.getInstance(project).getElementFactory().createExpressionFromText( + newref + "()", expression)); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + } + else if (conversion instanceof PsiField) { + if (resolved instanceof PsiField) { + try { + expression.replace( + JavaPsiFacade.getInstance(project).getElementFactory().createExpressionFromText(newref, expression)); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + else { + final PsiElement parent = Util.getEssentialParent(expression); + + if (parent instanceof PsiMethodCallExpression) { + try { + parent.replace( + JavaPsiFacade.getInstance(project).getElementFactory().createExpressionFromText(newref, expression)); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + } + } + } + } + + static void migratePsiMemeberType(final PsiElement element, final Project project, PsiType migratedType) { + try { + if (!migratedType.isValid()) { + migratedType = JavaPsiFacade.getElementFactory(project).createTypeByFQClassName(migratedType.getCanonicalText()); + } + final PsiTypeElement typeElement = + JavaPsiFacade.getInstance(project).getElementFactory().createTypeElement(migratedType); + if (element instanceof PsiMethod) { + final PsiTypeElement returnTypeElement = ((PsiMethod)element).getReturnTypeElement(); + if (returnTypeElement != null) { + returnTypeElement.replace(typeElement); + } + } + else if (element instanceof PsiVariable) { + final PsiTypeElement varTypeElement = ((PsiVariable)element).getTypeElement(); + if (varTypeElement != null) { + varTypeElement.replace(typeElement); + } + } + else { + LOG.error("Must not happen: " + element.getClass().getName()); + } + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + + static void replaceNewExpressionType(final Project project, final PsiNewExpression expression, final Map.Entry info) { + final PsiType changeType = info.getValue(); + if (changeType != null) { + try { + final PsiJavaCodeReferenceElement classReference = expression.getClassOrAnonymousClassReference(); + final PsiType componentType = changeType.getDeepComponentType(); + if (classReference != null) { + replaceTypeWithClassReferenceOrKeyword(project, componentType, classReference); + } + else { + final PsiElement typeKeyword = getTypeKeyword(expression); + if (typeKeyword != null) { + replaceTypeWithClassReferenceOrKeyword(project, componentType, typeKeyword); + } + } + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + } + + private static void replaceTypeWithClassReferenceOrKeyword(Project project, PsiType componentType, PsiElement typePlace) { + final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); + if (componentType instanceof PsiClassType) { + typePlace.replace(factory.createReferenceElementByType((PsiClassType)componentType)); + } else { + typePlace.replace(getTypeKeyword(((PsiNewExpression)factory.createExpressionFromText("new " + componentType.getPresentableText() + "[0]", typePlace)))); + } + } + + private static PsiElement getTypeKeyword(PsiNewExpression expression) { + return ((CompositeElement)expression).findChildByRoleAsPsiElement(ChildRole.TYPE_KEYWORD); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationRules.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationRules.java new file mode 100644 index 000000000000..da6f08d8bd3b --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationRules.java @@ -0,0 +1,93 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.*; +import com.intellij.psi.search.SearchScope; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.rules.*; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.LinkedList; + +/** + * Created by IntelliJ IDEA. + * User: db + * Date: Oct 2, 2004 + * Time: 9:24:23 PM + * To change this template use File | Settings | File Templates. + */ +public class TypeMigrationRules { + private final LinkedList myConversionRules = new LinkedList(); + + private final PsiType myRootType; + private PsiType myMigrationRootType; + private SearchScope mySearchScope; + + public TypeMigrationRules(final PsiType root) { + myRootType = root; + myConversionRules.add(new RootTypeConversionRule()); + myConversionRules.addAll(Arrays.asList(Extensions.getExtensions(TypeConversionRule.EP_NAME))); + } + + public void setMigrationRootType(PsiType migrationRootType) { + myMigrationRootType = migrationRootType; + } + + public PsiType getRootType() { + return myRootType; + } + + public PsiType getMigrationRootType() { + return myMigrationRootType; + } + + public void addConversionDescriptor(TypeConversionRule rule) { + myConversionRules.add(rule); + } + + @NonNls + @Nullable + public TypeConversionDescriptorBase findConversion(final PsiType from, final PsiType to, PsiMember member, final PsiExpression context, final boolean isCovariantPosition, + final TypeMigrationLabeler labeler) { + final TypeConversionDescriptorBase conversion = findConversion(from, to, member, context, labeler); + if (conversion != null) return conversion; + + if (isCovariantPosition) { + if (to instanceof PsiEllipsisType) { + if (TypeConversionUtil.isAssignable(((PsiEllipsisType)to).getComponentType(), from)) return new TypeConversionDescriptorBase(); + } + if (TypeConversionUtil.isAssignable(to, from)) return new TypeConversionDescriptorBase(); + } + if (!isCovariantPosition && TypeConversionUtil.isAssignable(from, to)) return new TypeConversionDescriptorBase(); + return null; + } + + @Nullable + public TypeConversionDescriptorBase findConversion(PsiType from, PsiType to, PsiMember member, PsiExpression context, TypeMigrationLabeler labeler) { + for (TypeConversionRule descriptor : myConversionRules) { + final TypeConversionDescriptorBase conversion = descriptor.findConversion(from, to, member, context, labeler); + if (conversion != null) return conversion; + } + return null; + } + + public void setBoundScope(final SearchScope searchScope) { + mySearchScope = searchScope; + } + + public SearchScope getSearchScope() { + return mySearchScope; + } + + @Nullable + public Pair bindTypeParameters(final PsiType from, final PsiType to, final PsiMethod method, final PsiExpression context, final TypeMigrationLabeler labeler) { + for (TypeConversionRule conversionRule : myConversionRules) { + final Pair typePair = conversionRule.bindTypeParameters(from, to, method, context, labeler); + if (typePair != null) return typePair; + } + return null; + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationStatementProcessor.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationStatementProcessor.java new file mode 100644 index 000000000000..dc7a470a4363 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationStatementProcessor.java @@ -0,0 +1,505 @@ +/* + * User: anna + * Date: 04-Apr-2008 + */ +package com.intellij.refactoring.typeMigration; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.Map; + +class TypeMigrationStatementProcessor extends JavaRecursiveElementVisitor { + private final PsiElement myStatement; + private final TypeMigrationLabeler myLabeler; + private static final Logger LOG = Logger.getInstance("#" + TypeMigrationStatementProcessor.class.getName()); + private final TypeEvaluator myTypeEvaluator; + + public TypeMigrationStatementProcessor(final PsiElement expression, TypeMigrationLabeler labeler) { + myStatement = expression; + myLabeler = labeler; + myTypeEvaluator = myLabeler.getTypeEvaluator(); + } + + @Override + public void visitAssignmentExpression(PsiAssignmentExpression expression) { + super.visitAssignmentExpression(expression); + + final PsiExpression lExpression = expression.getLExpression(); + final TypeView left = new TypeView(lExpression); + + final PsiExpression rExpression = expression.getRExpression(); + if (rExpression == null) return; + final TypeView right = new TypeView(rExpression); + + final IElementType sign = expression.getOperationTokenType(); + if (sign != JavaTokenType.EQ) { + final IElementType binaryOperator = TypeConversionUtil.convertEQtoOperation(sign); + if (!TypeConversionUtil.isBinaryOperatorApplicable(binaryOperator, left.getType(), right.getType(), false)) { + if (left.isChanged()) { + findConversionOrFail(expression, lExpression, left.getTypePair()); + } + if (right.isChanged()) { + findConversionOrFail(expression, rExpression, right.getTypePair()); + } + return; + } + } + + switch (TypeInfection.getInfection(left, right)) { + case TypeInfection.NONE_INFECTED: + break; + + case TypeInfection.LEFT_INFECTED: + myLabeler.migrateExpressionType(rExpression, left.getType(), myStatement, TypeConversionUtil.isAssignable(left.getType(), right.getType()), true); + break; + + case TypeInfection.RIGHT_INFECTED: + myLabeler.migrateExpressionType(lExpression, right.getType(), myStatement, TypeConversionUtil.isAssignable(left.getType(), right.getType()), false); + break; + + case TypeInfection.BOTH_INFECTED: + addTypeUsage(lExpression); + addTypeUsage(rExpression); + break; + + default: + LOG.error("Must not happen."); + } + } + + @Override + public void visitArrayAccessExpression(final PsiArrayAccessExpression expression) { + super.visitArrayAccessExpression(expression); + final PsiExpression indexExpression = expression.getIndexExpression(); + if (indexExpression != null) { + checkIndexExpression(indexExpression); + } + final TypeView typeView = new TypeView(expression.getArrayExpression()); + if (typeView.isChanged() && typeView.getType() instanceof PsiClassType) { + final TypeConversionDescriptorBase conversion = + myLabeler.getRules().findConversion(typeView.getTypePair().first, typeView.getType(), null, expression, false, myLabeler); + + if (conversion == null) { + myLabeler.markFailedConversion(typeView.getTypePair(), expression); + } + else { + myLabeler.setConversionMapping(expression, conversion); + myTypeEvaluator.setType(new TypeMigrationUsageInfo(expression), myTypeEvaluator.evaluateType(expression)); + } + + } + } + + @Override + public void visitSwitchLabelStatement(PsiSwitchLabelStatement statement) { + super.visitSwitchLabelStatement(statement); + final PsiExpression caseValue = statement.getCaseValue(); + if (caseValue != null) { + final TypeView typeView = new TypeView(caseValue); + if (typeView.isChanged()) { + final PsiSwitchStatement switchStatement = statement.getEnclosingSwitchStatement(); + if (switchStatement != null) { + final PsiExpression expression = switchStatement.getExpression(); + myLabeler.migrateExpressionType(expression, typeView.getType(), myStatement, false, false); + } + } + } + } + + @Override + public void visitInstanceOfExpression(final PsiInstanceOfExpression expression) { + super.visitInstanceOfExpression(expression); + final PsiTypeElement typeElement = expression.getCheckType(); + if (typeElement != null) { + final PsiExpression consideredExpression = expression.getOperand(); + final PsiType migrationType = myTypeEvaluator.evaluateType(consideredExpression); + final PsiType fixedType = typeElement.getType(); + if (migrationType != null && !TypeConversionUtil.isAssignable(migrationType, fixedType)) { + myLabeler.markFailedConversion(new Pair(fixedType, migrationType), consideredExpression); + } + } + } + + @Override + public void visitTypeCastExpression(final PsiTypeCastExpression expression) { + super.visitTypeCastExpression(expression); + final PsiTypeElement typeElement = expression.getCastType(); + if (typeElement != null) { + final PsiType fixedType = typeElement.getType(); + final PsiType migrationType = myTypeEvaluator.evaluateType(expression.getOperand()); + if (migrationType != null && !TypeConversionUtil.areTypesConvertible(migrationType, fixedType)) { + myLabeler.markFailedConversion(new Pair(fixedType, migrationType), expression); + } + } + } + + @Override + public void visitVariable(PsiVariable variable) { + super.visitVariable(variable); + + final PsiExpression initializer = variable.getInitializer(); + + if (initializer != null && initializer.getType() != null) { + processVariable(variable, initializer, null, null, null, false); + } + } + + @Override + public void visitReturnStatement(PsiReturnStatement statement) { // has to change method return type corresponding to new value type + super.visitReturnStatement(statement); + + final PsiMethod method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class); + final PsiExpression value = statement.getReturnValue(); + + if (method != null && value != null) { + final PsiType returnType = method.getReturnType(); + final PsiType valueType = myTypeEvaluator.evaluateType(value); + + if (returnType != null && valueType != null) { + myLabeler.addMigrationRoot(method, valueType, myStatement, TypeConversionUtil.isAssignable(returnType, valueType), true); + } + } + } + + @Override + public void visitReferenceExpression(PsiReferenceExpression expression) { + final PsiExpression qualifierExpression = expression.getQualifierExpression(); + + if (qualifierExpression != null && qualifierExpression.isPhysical()) { + qualifierExpression.accept(this); + + final TypeView qualifierView = new TypeView(qualifierExpression); + + if (qualifierView.isChanged()) { + final PsiMember member = (PsiMember)expression.advancedResolve(false).getElement(); + if (member == null) return; + final Pair typePair = qualifierView.getTypePair(); + + final TypeConversionDescriptorBase conversion = myLabeler.getRules().findConversion(typePair.getFirst(), typePair.getSecond(), member, expression, false, myLabeler); + + if (conversion == null) { + myLabeler.markFailedConversion(typePair, qualifierExpression); + } else { + final PsiElement parent = Util.getEssentialParent(expression); + if (parent instanceof PsiMethodCallExpression) { + myLabeler.setConversionMapping((PsiMethodCallExpression)parent, conversion); + myTypeEvaluator.setType(new TypeMigrationUsageInfo(parent), myTypeEvaluator.evaluateType((PsiExpression)parent)); + } else { + myLabeler.setConversionMapping(expression, conversion); + myTypeEvaluator.setType(new TypeMigrationUsageInfo(expression), myTypeEvaluator.evaluateType(expression)); + } + } + } + } + } + + @Override + public void visitIfStatement(PsiIfStatement statement) { + super.visitIfStatement(statement); + final PsiExpression condition = statement.getCondition(); + if (condition != null) { + final TypeView view = new TypeView(condition); + if (view.isChanged()) { //means that boolean condition becomes non-boolean + findConversionOrFail(condition, condition, view.getTypePair()); + } + } + } + + @Override + public void visitForeachStatement(final PsiForeachStatement statement) { + super.visitForeachStatement(statement); + final PsiExpression value = statement.getIteratedValue(); + final PsiParameter psiParameter = statement.getIterationParameter(); + if (value != null) { + final TypeView typeView = new TypeView(value); + PsiType psiType = typeView.getType(); + if (psiType instanceof PsiArrayType) { + psiType = ((PsiArrayType)psiType).getComponentType(); + } + else if (psiType instanceof PsiClassType) { + final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)psiType).resolveGenerics(); + final PsiClass psiClass = resolveResult.getElement(); + final Project project = statement.getProject(); + final PsiClass iterableClass = + JavaPsiFacade.getInstance(project).findClass("java.lang.Iterable", GlobalSearchScope.allScope(project)); + if (iterableClass == null) return; + if (!InheritanceUtil.isInheritorOrSelf(psiClass, iterableClass, true)) { + findConversionOrFail(value, value, typeView.getTypePair()); + return; + } + final PsiSubstitutor iterableParamSubstitutor = + TypeConversionUtil.getClassSubstitutor(iterableClass, psiClass, PsiSubstitutor.EMPTY); + LOG.assertTrue(iterableParamSubstitutor != null); + final PsiTypeParameter[] typeParameters = iterableClass.getTypeParameters(); + LOG.assertTrue(typeParameters.length == 1); + psiType = resolveResult.getSubstitutor().substitute(iterableParamSubstitutor.substitute(typeParameters[0])); + if (psiType instanceof PsiWildcardType) { + psiType = ((PsiWildcardType)psiType).getExtendsBound(); + } + } + else { + return; + } + final TypeView left = new TypeView(psiParameter, null, null); + if (TypeInfection.getInfection(left, typeView) == TypeInfection.LEFT_INFECTED) { + PsiType iterableType; + final PsiType typeViewType = typeView.getType(); + if (typeViewType instanceof PsiArrayType) { + iterableType = left.getType().createArrayType(); + } else { + final PsiClass iterableClass = PsiUtil.resolveClassInType(typeViewType); + LOG.assertTrue(iterableClass != null); + final PsiTypeParameter[] typeParameters = iterableClass.getTypeParameters(); + LOG.assertTrue(typeParameters.length == 1); + final Map substMap = Collections.singletonMap(typeParameters[0], left.getType()); + final PsiElementFactory factory = JavaPsiFacade.getElementFactory(iterableClass.getProject()); + iterableType = factory.createType(iterableClass, factory.createSubstitutor(substMap)); + } + myLabeler.migrateExpressionType(value, iterableType, myStatement, TypeConversionUtil.isAssignable(iterableType, typeViewType), true); + } else { + processVariable(psiParameter, value, psiType, null, null, false); + } + } + } + + @Override + public void visitNewExpression(final PsiNewExpression expression) { + super.visitNewExpression(expression); + final PsiExpression[] dimensions = expression.getArrayDimensions(); + for (PsiExpression dimension : dimensions) { + checkIndexExpression(dimension); + } + final PsiArrayInitializerExpression arrayInitializer = expression.getArrayInitializer(); + if (arrayInitializer != null) { + processArrayInitializer(arrayInitializer, expression); + } + } + + @Override + public void visitArrayInitializerExpression(final PsiArrayInitializerExpression expression) { + super.visitArrayInitializerExpression(expression); + processArrayInitializer(expression, expression); + } + + @Override + public void visitPostfixExpression(final PsiPostfixExpression expression) { + super.visitPostfixExpression(expression); + processUnaryExpression(expression, expression.getOperationSign()); + } + + @Override + public void visitPrefixExpression(final PsiPrefixExpression expression) { + super.visitPrefixExpression(expression); + processUnaryExpression(expression, expression.getOperationSign()); + } + + private void processUnaryExpression(final PsiExpression expression, PsiJavaToken sign) { + final TypeView typeView = new TypeView(expression); + if (typeView.isChanged()) { + if (!TypeConversionUtil.isUnaryOperatorApplicable(sign, typeView.getType())) { + findConversionOrFail(expression, expression, typeView.getTypePair()); + } + } + } + + private void findConversionOrFail(PsiExpression expression, PsiExpression toFail, Pair typePair) { + final TypeConversionDescriptorBase conversion = myLabeler.getRules().findConversion(typePair.getFirst(), typePair.getSecond(), null, expression, myLabeler); + if (conversion == null) { + myLabeler.markFailedConversion(typePair, toFail); + } + else { + myLabeler.setConversionMapping(expression, conversion); + myTypeEvaluator.setType(new TypeMigrationUsageInfo(expression), myTypeEvaluator.evaluateType(expression)); + } + } + + @Override + public void visitBinaryExpression(final PsiBinaryExpression expression) { + super.visitBinaryExpression(expression); + final PsiExpression lOperand = expression.getLOperand(); + final TypeView left = new TypeView(lOperand); + final PsiExpression rOperand = expression.getROperand(); + if (rOperand == null) return; + final TypeView right = new TypeView(rOperand); + if (!TypeConversionUtil.isBinaryOperatorApplicable(expression.getOperationSign().getTokenType(), left.getType(), right.getType(), false)) { + if (left.isChanged()) { + findConversionOrFail(expression, lOperand, left.getTypePair()); + } + if (right.isChanged()) { + findConversionOrFail(expression, rOperand, right.getTypePair()); + } + } + } + + private void processArrayInitializer(final PsiArrayInitializerExpression expression, final PsiExpression parentExpression) { + final PsiExpression[] initializers = expression.getInitializers(); + PsiType migrationType = null; + for (PsiExpression initializer : initializers) { + final TypeView typeView = new TypeView(initializer); + if (typeView.isChanged()) { + final PsiType type = typeView.getType(); + if (migrationType == null || !TypeConversionUtil.isAssignable(migrationType, type)) { + if (migrationType != null && !TypeConversionUtil.isAssignable(type, migrationType)) { + myLabeler.markFailedConversion(new Pair(parentExpression.getType(), type), parentExpression); + return; + } + migrationType = type; + } + } + } + final PsiType exprType = expression.getType(); + if (migrationType != null && exprType instanceof PsiArrayType) { + final boolean alreadyProcessed = TypeConversionUtil.isAssignable(((PsiArrayType)exprType).getComponentType(), migrationType); + myLabeler.migrateExpressionType(parentExpression, alreadyProcessed ? exprType : migrationType.createArrayType(), expression, alreadyProcessed, true); + } + } + + private void checkIndexExpression(final PsiExpression indexExpression) { + final PsiType indexType = myTypeEvaluator.evaluateType(indexExpression); + if (indexType != null && !TypeConversionUtil.isAssignable(PsiType.INT, indexType)) { + myLabeler.markFailedConversion(new Pair(indexExpression.getType(), indexType), indexExpression); + } + } + + @Override + public void visitMethodCallExpression(final PsiMethodCallExpression methodCallExpression) { + super.visitMethodCallExpression(methodCallExpression); + final JavaResolveResult resolveResult = methodCallExpression.resolveMethodGenerics(); + final PsiElement method = resolveResult.getElement(); + if (method instanceof PsiMethod) { + final PsiExpression[] psiExpressions = methodCallExpression.getArgumentList().getExpressions(); + final PsiParameter[] originalParams = ((PsiMethod)method).getParameterList().getParameters(); + final PsiSubstitutor evalSubstitutor = myTypeEvaluator.createMethodSubstitution(originalParams, psiExpressions, (PsiMethod)method, methodCallExpression); + for (int i = 0; i < psiExpressions.length; i++) { + PsiParameter originalParameter; + if (originalParams.length <= i) { + if (originalParams[originalParams.length - 1].isVarArgs()) { + originalParameter = originalParams[originalParams.length - 1]; + } else { + continue; + } + } + else { + originalParameter = originalParams[i]; + } + processVariable(originalParameter, psiExpressions[i], null, resolveResult.getSubstitutor(), evalSubstitutor, true); + } + final PsiExpression qualifier = methodCallExpression.getMethodExpression().getQualifierExpression(); + if (qualifier != null && qualifier.isPhysical() && !new TypeView(qualifier).isChanged()) { //substitute property otherwise + final PsiType qualifierType = qualifier.getType(); + if (qualifierType instanceof PsiClassType) { + final PsiClassType.ClassResolveResult classResolveResult = ((PsiClassType)qualifierType).resolveGenerics(); + final PsiType migrationType = + classResolveResult.getSubstitutor().substitute(evalSubstitutor.substitute(JavaPsiFacade.getElementFactory(myStatement.getProject()).createType(classResolveResult.getElement(), PsiSubstitutor.EMPTY))); + myLabeler.migrateExpressionType(qualifier, migrationType, myStatement, migrationType.equals(qualifierType), true); + } + } + } + } + + private void processVariable(final PsiVariable variable, + final PsiExpression value, + final PsiType migrationType, + final PsiSubstitutor varSubstitutor, + final PsiSubstitutor evalSubstitutor, + final boolean isCovariantPosition) { + final TypeView right = new TypeView(value); + final TypeView left = new TypeView(variable, varSubstitutor, evalSubstitutor); + + switch (TypeInfection.getInfection(left, right)) { + case TypeInfection.NONE_INFECTED: + break; + + case TypeInfection.LEFT_INFECTED: + myLabeler.migrateExpressionType(value, left.getType(), myStatement, TypeConversionUtil.isAssignable(left.getType(), right.getType()), true); + break; + + case TypeInfection.RIGHT_INFECTED: + PsiType psiType = migrationType != null ? migrationType : right.getType(); + if (!myLabeler.addMigrationRoot(variable, psiType, myStatement, TypeConversionUtil.isAssignable(left.getType(), psiType), true)) { + myLabeler.convertExpression(value, psiType, left.getType(), isCovariantPosition); + } + break; + + case TypeInfection.BOTH_INFECTED: + addTypeUsage(variable); + break; + + default: + LOG.error("Must not happen."); + } + } + + + private void addTypeUsage(final PsiElement typedElement) { + if (typedElement instanceof PsiReferenceExpression) { + myLabeler.setTypeUsage(((PsiReferenceExpression)typedElement).resolve(), myStatement); + } + else if (typedElement instanceof PsiMethodCallExpression) { + myLabeler.setTypeUsage(((PsiMethodCallExpression)typedElement).resolveMethod(), myStatement); + } + else { + myLabeler.setTypeUsage(typedElement, myStatement); + } + } + + + private class TypeView { + final PsiType myOriginType; + final PsiType myType; + final boolean myChanged; + + public TypeView(@NotNull PsiExpression expr) { + PsiType exprType = expr.getType(); + exprType = exprType instanceof PsiEllipsisType ? ((PsiEllipsisType)exprType).toArrayType() : exprType; + myOriginType = exprType != null ? GenericsUtil.getVariableTypeByExpressionType(exprType) : null; + PsiType type = myTypeEvaluator.evaluateType(expr); + type = type instanceof PsiEllipsisType ? ((PsiEllipsisType)type).toArrayType() : type; + myType = type != null ? GenericsUtil.getVariableTypeByExpressionType(type) : null; + myChanged = (myOriginType == null || myType == null) ? false : !myType.equals(myOriginType); + } + + public TypeView(PsiVariable var, PsiSubstitutor varSubstitutor, PsiSubstitutor evalSubstitutor) { + myOriginType = varSubstitutor != null ? varSubstitutor.substitute(var.getType()) : var.getType(); + myType = evalSubstitutor != null + ? evalSubstitutor.substitute(myTypeEvaluator.getType(var)) + : myTypeEvaluator.getType(var); + myChanged = (myOriginType == null || myType == null) ? false : !myType.equals(myOriginType); + } + + public PsiType getType() { + return myType; + } + + public boolean isChanged() { + return myChanged; + } + + public Pair getTypePair() { + return new Pair(myOriginType, myType); + } + } + + private static class TypeInfection { + static final int NONE_INFECTED = 0; + static final int LEFT_INFECTED = 1; + static final int RIGHT_INFECTED = 2; + static final int BOTH_INFECTED = 3; + + static int getInfection(final TypeView left, final TypeView right) { + return (left.isChanged() ? 1 : 0) + (right.isChanged() ? 2 : 0); + } + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationViewDescriptor.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationViewDescriptor.java new file mode 100644 index 000000000000..e71109b644e9 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/TypeMigrationViewDescriptor.java @@ -0,0 +1,34 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.psi.PsiElement; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usageView.UsageViewDescriptor; +import org.jetbrains.annotations.NotNull; + +class TypeMigrationViewDescriptor implements UsageViewDescriptor { + + private final PsiElement myElement; + + public TypeMigrationViewDescriptor(PsiElement elements) { + myElement = elements; + } + + @NotNull + public PsiElement[] getElements() { + return new PsiElement[]{myElement}; + } + + public String getProcessedElementsHeader() { + return "Root for type migration"; + } + + public String getCodeReferencesText(int usagesCount, int filesCount) { + return RefactoringBundle.message("occurences.to.be.migrated", UsageViewBundle.getReferencesString(usagesCount, filesCount)); + } + + public String getCommentReferencesText(int usagesCount, int filesCount) { + return null; + } + +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/Util.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/Util.java new file mode 100644 index 000000000000..00d54c93e99f --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/Util.java @@ -0,0 +1,74 @@ +package com.intellij.refactoring.typeMigration; + +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; + +/** + * Created by IntelliJ IDEA. + * User: db + * Date: Nov 4, 2004 + * Time: 2:40:49 PM + * To change this template use File | Settings | File Templates. + */ +public class Util { + + public static PsiElement getEssentialParent (final PsiElement element){ + final PsiElement parent = element.getParent(); + + if (parent instanceof PsiParenthesizedExpression){ + return getEssentialParent(parent); + } + + return parent; + } + + public static PsiElement normalizeElement(final PsiElement element) { + if (element instanceof PsiMethod) { + final PsiMethod superMethod = ((PsiMethod) element).findDeepestSuperMethod(); + + return superMethod == null ? element : superMethod; + } + else if (element instanceof PsiParameter && element.getParent() instanceof PsiParameterList) { + final PsiMethod method = PsiTreeUtil.getParentOfType(element, PsiMethod.class); + + if (method != null) { + final int index = method.getParameterList().getParameterIndex(((PsiParameter)element)); + final PsiMethod superMethod = method.findDeepestSuperMethod(); + + if (superMethod != null) { + return superMethod.getParameterList().getParameters()[index]; + } + } + } + + return element; + } + + public static boolean canBeMigrated(final PsiElement e) { + if (e == null){ + return false; + } + + final PsiElement element = normalizeElement(e); + + if (!element.getManager().isInProject(element)) { + return false; + } + + final PsiType type = TypeMigrationLabeler.getElementType(element); + + if (type != null) { + final PsiType elemenType = type instanceof PsiArrayType ? type.getDeepComponentType() : type; + + if (elemenType instanceof PsiPrimitiveType) { + return !elemenType.equals(PsiType.VOID); + } + + final PsiClass aClass = ((PsiClassType)elemenType).resolve(); + + return aClass != null/* && !aClass.hasTypeParameters()*/; + } + + return false; + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/actions/ChangeTypeSignatureAction.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/actions/ChangeTypeSignatureAction.java new file mode 100644 index 000000000000..422a019fe7c3 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/actions/ChangeTypeSignatureAction.java @@ -0,0 +1,54 @@ +package com.intellij.refactoring.typeMigration.actions; + +import com.intellij.ide.DataManager; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.RefactoringActionHandler; +import com.intellij.refactoring.actions.BaseRefactoringAction; +import com.intellij.refactoring.typeMigration.ChangeTypeSignatureHandler; + +public class ChangeTypeSignatureAction extends BaseRefactoringAction { + public boolean isAvailableInEditorOnly() { + return false; + } + + public boolean isEnabledOnElements(PsiElement[] elements) { + Project currProject = PlatformDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext()); + + if (currProject == null) { + return false; + } + + if (elements.length > 1) return false; + + for (PsiElement element : elements) { + if (!(element instanceof PsiMethod || element instanceof PsiVariable)) { + return false; + } + } + + return true; + } + + protected boolean isAvailableOnElementInEditor(final PsiElement element, final Editor editor) { + final Document document = editor.getDocument(); + final PsiFile file = PsiDocumentManager.getInstance(element.getProject()).getPsiFile(document); + if (file != null) { + final PsiElement psiElement = file.findElementAt(editor.getCaretModel().getOffset()); + final PsiReferenceParameterList referenceParameterList = PsiTreeUtil.getParentOfType(psiElement, PsiReferenceParameterList.class); + if (referenceParameterList != null) { + return referenceParameterList.getTypeArguments().length > 0; + } + } + return true; + } + + public RefactoringActionHandler getHandler(DataContext dataContext) { + return new ChangeTypeSignatureHandler(); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/RootTypeConversionRule.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/RootTypeConversionRule.java new file mode 100644 index 000000000000..54e2c506cf83 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/RootTypeConversionRule.java @@ -0,0 +1,81 @@ +/* + * User: anna + * Date: 08-Aug-2008 + */ +package com.intellij.refactoring.typeMigration.rules; + +import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; + +public class RootTypeConversionRule extends TypeConversionRule { + + public TypeConversionDescriptorBase findConversion(final PsiType from, final PsiType to, final PsiMember member, final PsiExpression context, + final TypeMigrationLabeler labeler) { + if (to instanceof PsiClassType && from instanceof PsiClassType) { + final PsiClass targetClass = ((PsiClassType)to).resolve(); + if (targetClass != null && member instanceof PsiMethod && member.isPhysical()) { + PsiMethod method = (PsiMethod)member; + PsiMethod replacer = targetClass.findMethodBySignature(method, true); + if (replacer == null) { + for (PsiMethod superMethod : method.findDeepestSuperMethods()) { + replacer = targetClass.findMethodBySignature(superMethod, true); + if (replacer != null) { + method = superMethod; + break; + } + } + } + if (replacer != null && TypeConversionUtil.areTypesConvertible(method.getReturnType(), replacer.getReturnType())) { + final PsiElement parent = context.getParent(); + if (context instanceof PsiReferenceExpression && parent instanceof PsiMethodCallExpression) { + final JavaResolveResult resolveResult = ((PsiReferenceExpression)context).advancedResolve(false); + final PsiSubstitutor aSubst; + final PsiReferenceExpression methodExpression = ((PsiMethodCallExpression)parent).getMethodExpression(); + final PsiExpression qualifier = methodExpression.getQualifierExpression(); + final PsiClass substitutionClass = method.getContainingClass(); + if (qualifier != null) { + final PsiType evaluatedQualifierType = labeler.getTypeEvaluator().evaluateType(qualifier); + if (evaluatedQualifierType instanceof PsiClassType) { + aSubst = ((PsiClassType)evaluatedQualifierType).resolveGenerics().getSubstitutor(); + } + else { + aSubst = PsiSubstitutor.EMPTY; + } + } + else { + aSubst = TypeConversionUtil.getClassSubstitutor(member.getContainingClass(), substitutionClass, PsiSubstitutor.EMPTY); + } + + final PsiParameter[] originalParams = ((PsiMethod)member).getParameterList().getParameters(); + final PsiParameter[] migrationParams = replacer.getParameterList().getParameters(); + final PsiExpression[] actualParams = ((PsiMethodCallExpression)parent).getArgumentList().getExpressions(); + + assert originalParams.length == migrationParams.length; + final PsiSubstitutor methodTypeParamsSubstitutor = + labeler.getTypeEvaluator().createMethodSubstitution(originalParams, actualParams, method, (PsiReferenceExpression)context, aSubst != null ? aSubst : PsiSubstitutor.EMPTY, true); + for (int i = 0; i < originalParams.length; i++) { + final PsiType originalType = resolveResult.getSubstitutor().substitute(originalParams[i].getType()); + + PsiType type = migrationParams[i].getType(); + if (InheritanceUtil.isInheritorOrSelf(targetClass, substitutionClass, true)) { + final PsiSubstitutor superClassSubstitutor = + TypeConversionUtil.getClassSubstitutor(substitutionClass, targetClass, PsiSubstitutor.EMPTY); + assert (superClassSubstitutor != null); + type = superClassSubstitutor.substitute(type); + } + + if (!originalType.equals(type)) { + labeler.migrateExpressionType(actualParams[i], methodTypeParamsSubstitutor.substitute(type), context, originalType.equals(type), true); + } + } + } + return new TypeConversionDescriptorBase(); + } + } + } + return null; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/TypeConversionRule.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/TypeConversionRule.java new file mode 100644 index 000000000000..28c2bf79d28d --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/rules/TypeConversionRule.java @@ -0,0 +1,32 @@ +/* + * User: anna + * Date: 08-Aug-2008 + */ +package com.intellij.refactoring.typeMigration.rules; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMember; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; +import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import org.jetbrains.annotations.Nullable; + +public abstract class TypeConversionRule { + public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.conversion.rule"); + @Nullable + public abstract TypeConversionDescriptorBase findConversion(final PsiType from, + final PsiType to, + final PsiMember member, + final PsiExpression context, + final TypeMigrationLabeler labeler); + + + @Nullable + public Pair bindTypeParameters(PsiType from, PsiType to, final PsiMethod method, final PsiExpression context, + final TypeMigrationLabeler labeler) { + return null; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/FailedConversionsDialog.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/FailedConversionsDialog.java new file mode 100644 index 000000000000..991bfc75342c --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/FailedConversionsDialog.java @@ -0,0 +1,78 @@ +/** + * created at Sep 12, 2001 + * @author Jeka + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.refactoring.RefactoringBundle; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; + +import org.jetbrains.annotations.NonNls; + +public class FailedConversionsDialog extends DialogWrapper { + private final String[] myConflictDescriptions; + public static final int VIEW_USAGES_EXIT_CODE = NEXT_USER_EXIT_CODE; + + public FailedConversionsDialog(String[] conflictDescriptions, Project project) { + super(project, true); + myConflictDescriptions = conflictDescriptions; + setTitle(RefactoringBundle.message("usages.detected.title")); + setOKButtonText(RefactoringBundle.message("ignore.button")); + getOKAction().putValue(Action.MNEMONIC_KEY, new Integer('I')); + init(); + } + + protected Action[] createActions() { + return new Action[]{getOKAction(), new ViewUsagesAction(), new CancelAction()}; + } + + protected JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + @NonNls final String contentType = "text/html"; + final JEditorPane messagePane = new JEditorPane(contentType, ""); + messagePane.setEditable(false); + JScrollPane scrollPane = new JScrollPane(messagePane); + scrollPane.setPreferredSize(new Dimension(500, 400)); + panel.add(new JLabel(RefactoringBundle.message("the.following.problems.were.found")), BorderLayout.NORTH); + panel.add(scrollPane, BorderLayout.CENTER); + + @NonNls StringBuffer buf = new StringBuffer(); + for (String description : myConflictDescriptions) { + buf.append(description); + buf.append("

"); + } + messagePane.setText(buf.toString()); + return panel; + } + + protected String getDimensionServiceKey() { + return "#com.intellij.refactoring.typeMigration.ui.FailedConversionsDialog"; + } + + private class CancelAction extends AbstractAction { + public CancelAction() { + super(RefactoringBundle.message("cancel.button")); + } + + public void actionPerformed(ActionEvent e) { + doCancelAction(); + } + } + + private class ViewUsagesAction extends AbstractAction { + public ViewUsagesAction() { + super(RefactoringBundle.message("view.usages")); + putValue(Action.MNEMONIC_KEY, new Integer('V')); + putValue(DialogWrapper.DEFAULT_ACTION, Boolean.TRUE); + } + + public void actionPerformed(ActionEvent e) { + close(VIEW_USAGES_EXIT_CODE); + } + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationConflictsPanel.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationConflictsPanel.java new file mode 100644 index 000000000000..e982ada6b0e9 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationConflictsPanel.java @@ -0,0 +1,29 @@ +/* + * User: anna + * Date: 26-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.openapi.project.Project; +import com.intellij.packageDependencies.ui.UsagesPanel; +import com.intellij.usageView.UsageInfo; +import com.intellij.usages.UsageInfoToUsageConverter; + +public class MigrationConflictsPanel extends UsagesPanel{ + public MigrationConflictsPanel(Project project) { + super(project); + } + + public String getInitialPositionText() { + return "No migration conflicts found"; + } + + public String getCodeUsagesString() { + return "Found migration conflicts"; + } + + @Override + public void showUsages(final UsageInfoToUsageConverter.TargetElementsDescriptor descriptor, final UsageInfo[] usageInfos) { + super.showUsages(descriptor, usageInfos); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationNode.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationNode.java new file mode 100644 index 000000000000..448310a034a6 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationNode.java @@ -0,0 +1,113 @@ +/* + * User: anna + * Date: 25-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiType; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.ui.DuplicateNodeRenderer; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class MigrationNode extends AbstractTreeNode implements DuplicateNodeRenderer.DuplicatableNode { + private final TypeMigrationUsageInfo myInfo; + private MigrationNode myDuplicatedNode; + private List myCachedChildren; + private final TypeMigrationLabeler myLabeler; + private final PsiType myMigrationType; + private final TypeMigrationTreeBuilder myBuilder; + private final HashMap> myProcessed; + private final HashSet myParents; + + public MigrationNode(final Project project, + final TypeMigrationUsageInfo info, + final PsiType migrationType, + final TypeMigrationLabeler labeler, + final TypeMigrationTreeBuilder builder, + final HashSet parents, + final HashMap> processed) { + super(project, info); + myLabeler = labeler; + myMigrationType = migrationType; + myBuilder = builder; + myProcessed = processed; + myParents = parents; + + Set alreadyAdded = myProcessed.get(info); + if (alreadyAdded == null) { + alreadyAdded = new HashSet(); + myProcessed.put(info, alreadyAdded); + myInfo = info; + } + else { + final MigrationNode duplicate = alreadyAdded.iterator().next(); + myInfo = duplicate.getInfo(); + myDuplicatedNode = duplicate; + } + alreadyAdded.add(this); + } + + public TypeMigrationUsageInfo getInfo() { + return myInfo; + } + + @NotNull + public Collection getChildren() { + if (myCachedChildren == null) { + myCachedChildren = new ArrayList(); + + final PsiElement element = myInfo.getElement(); + if (element != null) { + myLabeler.setCurrentRoot(myInfo); + + myLabeler.migrateRoot(element, myMigrationType, myLabeler.markRootUsages(element, myMigrationType)); + + final HashSet> roots = myLabeler.getRootsTree().get(myInfo); + if (roots != null) { + for (Pair root : roots) { + + final TypeMigrationUsageInfo info = root.getFirst(); + + if (myParents.contains(info)) continue; + final HashSet parents = new HashSet(myParents); + parents.add(info); + + final MigrationNode migrationNode = + new MigrationNode(getProject(), info, root.getSecond(), myLabeler, myBuilder, parents, myProcessed); + + if (myInfo.isExcluded()) { + info.setExcluded(true); + } + + myCachedChildren.add(migrationNode); + } + } + } + } + return myCachedChildren; + } + + public boolean areChildrenInitialized() { + return myCachedChildren != null; + } + + protected void update(final PresentationData presentation) { + + } + + public MigrationNode getDuplicate() { + return myDuplicatedNode; + } + + public boolean hasDuplicate() { + return myDuplicatedNode != null; + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationPanel.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationPanel.java new file mode 100644 index 000000000000..03b8036f4b8a --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationPanel.java @@ -0,0 +1,491 @@ +/* + * User: anna + * Date: 24-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.CommonBundle; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.help.HelpManager; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Splitter; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.ReadonlyStatusHandler; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.presentation.java.SymbolPresentationUtil; +import com.intellij.psi.util.PsiFormatUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.typeMigration.ChangeTypeSignatureHandler; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.TypeMigrationProcessor; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.ui.*; +import com.intellij.ui.content.Content; +import com.intellij.ui.treeStructure.Tree; +import com.intellij.usageView.UsageInfo; +import com.intellij.usageView.UsageViewBundle; +import com.intellij.usageView.UsageViewManager; +import com.intellij.usages.TextChunk; +import com.intellij.usages.UsageInfoToUsageConverter; +import com.intellij.usages.UsagePresentation; +import com.intellij.util.EditSourceOnDoubleClickHandler; +import com.intellij.util.containers.HashSet; +import com.intellij.util.ui.UIUtil; +import com.intellij.util.ui.tree.TreeUtil; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.Set; + +public class MigrationPanel extends JPanel implements Disposable { + @NonNls private static final String MIGRATION_USAGES = "migration.usages"; + private static final DataKey MIGRATION_USAGES_KEYS = DataKey.create(MIGRATION_USAGES); + + private final PsiElement myInitialRoot; + private final TypeMigrationLabeler myLabeler; + + + private final MyTree myRootsTree; + private static final Logger LOG = Logger.getInstance("#" + MigrationPanel.class.getName()); + private final Project myProject; + private final boolean myPreviewUsages; + private Content myContent; + private final MigrationUsagesPanel myUsagesPanel; + private final MigrationConflictsPanel myConflictsPanel; + + public MigrationPanel(final PsiElement root, TypeMigrationLabeler labeler, final Project project, final boolean previewUsages) { + super(new BorderLayout()); + myInitialRoot = root; + myLabeler = labeler; + myProject = project; + myPreviewUsages = previewUsages; + + myRootsTree = new MyTree(new DefaultTreeModel(new DefaultMutableTreeNode())); + final TypeMigrationTreeBuilder builder = new TypeMigrationTreeBuilder(myRootsTree, project); + final MigrationRootNode currentRoot = new MigrationRootNode(project, myLabeler, builder, root, myPreviewUsages); + builder.setRoot(currentRoot); + initTree(myRootsTree); + myRootsTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { + public void valueChanged(final TreeSelectionEvent e) { + selectionChanged(); + } + }); + + final Splitter treeSplitter = new Splitter(); + Disposer.register(this, new Disposable() { + public void dispose() { + treeSplitter.dispose(); + } + }); + treeSplitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myRootsTree)); + + + myUsagesPanel = new MigrationUsagesPanel(myProject); + treeSplitter.setSecondComponent(myUsagesPanel); + Disposer.register(this, myUsagesPanel); + + add(createToolbar(), BorderLayout.SOUTH); + + final Splitter conflictsSplitter = new Splitter(true, .8f); + Disposer.register(this, new Disposable() { + public void dispose() { + conflictsSplitter.dispose(); + } + }); + conflictsSplitter.setFirstComponent(treeSplitter); + myConflictsPanel = new MigrationConflictsPanel(myProject); + conflictsSplitter.setSecondComponent(myConflictsPanel); + add(conflictsSplitter, BorderLayout.CENTER); + Disposer.register(this, myConflictsPanel); + + builder.addSubtreeToUpdate((DefaultMutableTreeNode)myRootsTree.getModel().getRoot(), new Runnable() { + public void run() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (builder.isDisposed()) return; + myRootsTree.expandPath(new TreePath(myRootsTree.getModel().getRoot())); + final Collection children = currentRoot.getChildren(); + if (!children.isEmpty()) { + builder.select(children.iterator().next()); + } + } + }); + } + }); + + Disposer.register(this, builder); + } + + private void selectionChanged() { + myConflictsPanel.setToInitialPosition(); + myUsagesPanel.setToInitialPosition(); + final DefaultMutableTreeNode[] migrationNodes = myRootsTree.getSelectedNodes(DefaultMutableTreeNode.class, null); + if (migrationNodes == null || migrationNodes.length == 0) return; + final Object userObject = migrationNodes[0].getUserObject(); + if (userObject instanceof MigrationNode) { + final MigrationNode migrationNode = (MigrationNode)userObject; + final UsageInfo[] failedUsages = myLabeler.getFailedUsages(); + if (failedUsages.length > 0) { + myConflictsPanel.showUsages(new UsageInfoToUsageConverter.TargetElementsDescriptor(new PsiElement[0]), failedUsages); + } + final AbstractTreeNode rootNode = migrationNode.getParent(); + if (rootNode instanceof MigrationNode) { + myUsagesPanel.showRootUsages(((MigrationNode)rootNode).getInfo(), migrationNode.getInfo(), myLabeler); + } + } + } + + private JComponent createToolbar() { + final JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gc = new GridBagConstraints(GridBagConstraints.RELATIVE, 0, 1, 1, 0, 1, GridBagConstraints.NORTHWEST, + GridBagConstraints.NONE, new Insets(5, 10, 5, 0), 0, 0); + final JButton performButton = new JButton(RefactoringBundle.message("type.migration.migrate.button.text")); + performButton.addActionListener(new ActionListener() { + private void expandTree(MigrationNode migrationNode) { + if (!migrationNode.getInfo().isExcluded() || migrationNode.areChildrenInitialized()) { //do not walk into excluded collapsed nodes: nothing to migrate can be found + final Collection nodes = migrationNode.getChildren(); + for (AbstractTreeNode node : nodes) { + expandTree((MigrationNode)node); + } + } + } + + public void actionPerformed(final ActionEvent e) { + final Object root = myRootsTree.getModel().getRoot(); + if (root instanceof DefaultMutableTreeNode) { + final Object userObject = ((DefaultMutableTreeNode)root).getUserObject(); + if (userObject instanceof MigrationRootNode) { + ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { + public void run() { + new WriteCommandAction(myProject) { + protected void run(Result result) throws Throwable { + final Collection children = ((MigrationRootNode)userObject).getChildren(); + for (AbstractTreeNode child : children) { + expandTree((MigrationNode)child); + } + final TypeMigrationUsageInfo[] usages = myLabeler.getMigratedUsages(); + final HashSet files = new HashSet(); + for (TypeMigrationUsageInfo usage : usages) { + if (!usage.isExcluded()) { + final PsiElement element = usage.getElement(); + if (element != null) { + files.add(element.getContainingFile().getVirtualFile()); + } + } + } + if (ReadonlyStatusHandler.getInstance(myProject). + ensureFilesWritable(VfsUtil.toVirtualFileArray(files)).hasReadonlyFiles()) return; + + TypeMigrationProcessor.change(myLabeler, usages); + } + }.execute(); + } + }, "Type Migration", false, myProject); + } + } + UsageViewManager.getInstance(myProject).closeContent(myContent); + } + }); + panel.add(performButton, gc); + final JButton closeButton = new JButton(CommonBundle.getCancelButtonText()); + closeButton.addActionListener(new ActionListener() { + public void actionPerformed(final ActionEvent e) { + UsageViewManager.getInstance(myProject).closeContent(myContent); + + } + }); + panel.add(closeButton, gc); + final JButton rerunButton = new JButton(RefactoringBundle.message("type.migration.rerun.button.text")); + rerunButton.addActionListener(new ActionListener() { + public void actionPerformed(final ActionEvent e) { + UsageViewManager.getInstance(myProject).closeContent(myContent); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + ChangeTypeSignatureHandler.invoke(myProject, myInitialRoot, myLabeler.getRules(), null); + } + }); + } + }); + panel.add(rerunButton, gc); + final JButton helpButton = new JButton(CommonBundle.getHelpButtonText()); + helpButton.addActionListener(new ActionListener() { + public void actionPerformed(final ActionEvent e) { + HelpManager.getInstance().invokeHelp("reference.typeMigrationPreview"); + } + }); + gc.weightx = 1; + panel.add(helpButton, gc); + + return panel; + } + + private void initTree(final Tree tree) { + final MigrationRootsTreeCellRenderer rootsTreeCellRenderer = new MigrationRootsTreeCellRenderer(); + rootsTreeCellRenderer.setOpaque(false); + tree.setCellRenderer(rootsTreeCellRenderer); + tree.setRootVisible(false); + tree.setShowsRootHandles(true); + UIUtil.setLineStyleAngled(tree); + + TreeToolTipHandler.install(tree); + TreeUtil.installActions(tree); + TreeUtil.expandAll(tree); + SmartExpander.installOn(tree); + EditSourceOnDoubleClickHandler.install(tree); + new TreeSpeedSearch(tree); + PopupHandler.installUnknownPopupHandler(tree, createTreePopupActions(), ActionManager.getInstance()); + } + + private ActionGroup createTreePopupActions() { + final DefaultActionGroup group = new DefaultActionGroup(); + //group.add(new PerformRefactoringAction()); + group.add(new ExcludeAction()); + group.add(new IncludeAction()); + group.addSeparator(); + final ActionManager actionManager = ActionManager.getInstance(); + group.add(actionManager.getAction(IdeActions.ACTION_EDIT_SOURCE)); + group.add(actionManager.getAction(IdeActions.GROUP_VERSION_CONTROLS)); + return group; + } + + + public void dispose() { + } + + public void setContent(final Content content) { + myContent = content; + Disposer.register(content, this); + } + + private static class MyTree extends Tree implements DataProvider { + private MyTree(final TreeModel treemodel) { + super(treemodel); + setOpaque(false); + } + + @Override + protected void paintComponent(final Graphics g) { + DuplicateNodeRenderer.paintDuplicateNodesBackground(g, this); + super.paintComponent(g); + } + + public Object getData(@NonNls final String dataId) { + if (DataConstants.PSI_ELEMENT.equals(dataId)) { + final DefaultMutableTreeNode[] selectedNodes = getSelectedNodes(DefaultMutableTreeNode.class, null); + return selectedNodes != null && selectedNodes.length == 1 && selectedNodes[0].getUserObject() instanceof MigrationNode + ? ((MigrationNode)selectedNodes[0].getUserObject()).getInfo().getElement() : null; + } + if (MIGRATION_USAGES.equals(dataId)) { + DefaultMutableTreeNode[] selectedNodes = getSelectedNodes(DefaultMutableTreeNode.class, null); + if (selectedNodes == null) return null; + final Set usageInfos = new HashSet(); + for (DefaultMutableTreeNode selectedNode : selectedNodes) { + final Object userObject = selectedNode.getUserObject(); + if (userObject instanceof MigrationNode) { + collectInfos(usageInfos, (MigrationNode)userObject); + } + } + return usageInfos.toArray(new TypeMigrationUsageInfo[usageInfos.size()]); + } + return null; + } + + private static void collectInfos(final Set usageInfos, final MigrationNode currentNode) { + usageInfos.add(currentNode.getInfo()); + if (!currentNode.areChildrenInitialized()) return; + final Collection nodes = currentNode.getChildren(); + for (AbstractTreeNode node : nodes) { + collectInfos(usageInfos, (MigrationNode)node); + } + } + } + + /*private class PerformRefactoringAction extends AnAction { + private PerformRefactoringAction() { + super(RefactoringBundle.message("type.migration.migrate.button.text")); + } + + public void actionPerformed(final AnActionEvent e) { + final DefaultMutableTreeNode[] nodes = myRootsTree.getSelectedNodes(DefaultMutableTreeNode.class, null); + for (DefaultMutableTreeNode node : nodes) { + final Object userObject = node.getUserObject(); + if (userObject instanceof MigrationNode) { + migrate(((MigrationNode)userObject)); + } + } + } + + @Override + public void update(final AnActionEvent e) { + super.update(e); + final MigrationNode[] selectedNodes = myRootsTree.getSelectedNodes(MigrationNode.class, null); + e.getPresentation().setEnabled(selectedNodes != null && selectedNodes.length == 1); + } + }*/ + + private class ExcludeAction extends ExcludeIncludeActionBase { + public ExcludeAction() { + super(RefactoringBundle.message("type.migration.exclude.action.text")); + registerCustomShortcutSet(CommonShortcuts.DELETE, myRootsTree); + } + + protected void processUsage(final TypeMigrationUsageInfo usageInfo) { + usageInfo.setExcluded(true); + } + } + + private class IncludeAction extends ExcludeIncludeActionBase { + public IncludeAction() { + super(RefactoringBundle.message("type.migration.include.action.text")); + registerCustomShortcutSet(CommonShortcuts.INSERT, myRootsTree); + } + + protected void processUsage(final TypeMigrationUsageInfo usageInfo) { + usageInfo.setExcluded(false); + } + + @Override + public void update(final AnActionEvent e) { + final Presentation presentation = e.getPresentation(); + presentation.setEnabled(false); + final DefaultMutableTreeNode[] selectedNodes = myRootsTree.getSelectedNodes(DefaultMutableTreeNode.class, null); + if (selectedNodes == null) return; + for (DefaultMutableTreeNode node : selectedNodes) { + final Object userObject = node.getUserObject(); + if (!(userObject instanceof MigrationNode)) return; + final AbstractTreeNode parent = ((MigrationNode)userObject).getParent(); //disable include if parent was excluded + if (parent instanceof MigrationNode && ((MigrationNode)parent).getInfo().isExcluded()) return; + } + presentation.setEnabled(true); + } + } + + private abstract class ExcludeIncludeActionBase extends AnAction { + protected abstract void processUsage(TypeMigrationUsageInfo usageInfo); + + protected ExcludeIncludeActionBase(final String text) { + super(text); + } + + @Nullable + private TypeMigrationUsageInfo[] getUsages(AnActionEvent context) { + return MIGRATION_USAGES_KEYS.getData(context.getDataContext()); + } + + public void update(AnActionEvent e) { + final TreePath[] selectionPaths = myRootsTree.getSelectionPaths(); + e.getPresentation().setEnabled(selectionPaths != null && selectionPaths.length > 0); + } + + public void actionPerformed(AnActionEvent e) { + final TypeMigrationUsageInfo[] usages = getUsages(e); + assert usages != null; + for (TypeMigrationUsageInfo usageInfo : usages) { + processUsage(usageInfo); + } + myRootsTree.repaint(); + } + } + + private static class MigrationRootsTreeCellRenderer extends ColoredTreeCellRenderer { + public void customizeCellRenderer(final JTree tree, + final Object value, + final boolean selected, + final boolean expanded, + final boolean leaf, + final int row, + final boolean hasFocus) { + final Object userObject = ((DefaultMutableTreeNode)value).getUserObject(); + if (!(userObject instanceof MigrationNode)) return; + final TypeMigrationUsageInfo usageInfo = ((MigrationNode)userObject).getInfo(); + if (usageInfo != null) { + final PsiElement element = usageInfo.getElement(); + if (element != null) { + PsiElement typeElement = null; + if (element instanceof PsiVariable) { + typeElement = ((PsiVariable)element).getTypeElement(); + } else if (element instanceof PsiMethod) { + typeElement = ((PsiMethod)element).getReturnTypeElement(); + } + if (typeElement == null) typeElement = element; + PsiDocumentManager.getInstance(element.getProject()).commitAllDocuments(); + final UsagePresentation presentation = UsageInfoToUsageConverter.convert(new UsageInfoToUsageConverter.TargetElementsDescriptor(typeElement), new UsageInfo(typeElement)).getPresentation(); + boolean isPrefix = true; //skip usage position + for (TextChunk chunk : presentation.getText()) { + if (!isPrefix) append(chunk.getText(), patchAttrs(usageInfo, SimpleTextAttributes.fromTextAttributes(chunk.getAttributes()))); + isPrefix = false; + } + setIcon(presentation.getIcon()); + + String location; + if (element instanceof PsiMember) { + location = SymbolPresentationUtil.getSymbolContainerText(element); + } + else { + final PsiMember member = PsiTreeUtil.getParentOfType(element, PsiMember.class); + if (member instanceof PsiField) { + location = PsiFormatUtil.formatVariable((PsiVariable)member, PsiFormatUtil + .SHOW_NAME | + PsiFormatUtil + .SHOW_CONTAINING_CLASS | + PsiFormatUtil + .SHOW_FQ_NAME, PsiSubstitutor.EMPTY); + } + else if (member instanceof PsiMethod) { + location = PsiFormatUtil.formatMethod((PsiMethod)member, PsiSubstitutor.EMPTY, PsiFormatUtil + .SHOW_NAME | + PsiFormatUtil + .SHOW_CONTAINING_CLASS | + PsiFormatUtil + .SHOW_FQ_NAME, PsiFormatUtil.SHOW_TYPE); + } + else if (member instanceof PsiClass) { + location = PsiFormatUtil.formatClass((PsiClass)member, PsiFormatUtil + .SHOW_NAME | + PsiFormatUtil + .SHOW_CONTAINING_CLASS | + PsiFormatUtil + .SHOW_FQ_NAME); + } + else { + location = null; + } + if (location != null) location = PsiBundle.message("aux.context.display", location); + } + if (location != null) { + append(location, SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + } else { + append(UsageViewBundle.message("node.invalid"), SimpleTextAttributes.ERROR_ATTRIBUTES); + } + } + } + + private static SimpleTextAttributes patchAttrs(TypeMigrationUsageInfo usageInfo, SimpleTextAttributes original) { + if (usageInfo.isExcluded()) { + original = new SimpleTextAttributes(original.getStyle() | SimpleTextAttributes.STYLE_STRIKEOUT, original.getFgColor(), original.getWaveColor()); + } + return original; + } + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationRootNode.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationRootNode.java new file mode 100644 index 000000000000..d6abbc4727da --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationRootNode.java @@ -0,0 +1,73 @@ +/* + * User: anna + * Date: 16-Apr-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.ide.util.treeView.AbstractTreeNode; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiType; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.ui.DuplicateNodeRenderer; +import com.intellij.util.containers.HashSet; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.*; + +public class MigrationRootNode extends AbstractTreeNode implements DuplicateNodeRenderer.DuplicatableNode { + private final TypeMigrationLabeler myLabeler; + private List myCachedChildren; + private final TypeMigrationTreeBuilder myBuilder; + private final PsiElement myRoot; + private final boolean myPreviewUsages; + + protected MigrationRootNode(Project project, + TypeMigrationLabeler labeler, + final TypeMigrationTreeBuilder builder, final PsiElement root, + final boolean previewUsages) { + super(project, labeler); + myLabeler = labeler; + myBuilder = builder; + myRoot = root; + myPreviewUsages = previewUsages; + } + + @NotNull + public Collection getChildren() { + if (myCachedChildren == null) { + myCachedChildren = new ArrayList(); + if (myPreviewUsages) { + for (Pair root : myLabeler.getMigrationRoots()) { + addRoot(root.getFirst(), root.getSecond()); + } + } + else { + addRoot(new TypeMigrationUsageInfo(myRoot), myLabeler.getRules().getMigrationRootType()); + } + } + return myCachedChildren; + } + + private void addRoot(TypeMigrationUsageInfo info, PsiType migrationType) { + final HashSet parents = new HashSet(); + parents.add(info); + final MigrationNode migrationNode = + new MigrationNode(getProject(), info, migrationType, myLabeler, myBuilder, parents, new HashMap>()); + + myCachedChildren.add(migrationNode); + } + + protected void update(final PresentationData presentation) { + + } + + public DefaultMutableTreeNode getDuplicate() { + return null; + } + +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationUsagesPanel.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationUsagesPanel.java new file mode 100644 index 000000000000..a4ba10d73b17 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/MigrationUsagesPanel.java @@ -0,0 +1,56 @@ +/* + * User: anna + * Date: 26-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.openapi.project.Project; +import com.intellij.packageDependencies.ui.UsagesPanel; +import com.intellij.psi.PsiElement; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; +import com.intellij.usageView.UsageInfo; +import com.intellij.usages.UsageInfoToUsageConverter; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class MigrationUsagesPanel extends UsagesPanel { + public MigrationUsagesPanel(Project project) { + super(project); + } + + public String getInitialPositionText() { + return "Select root to find reasons to migrate"; + } + + public String getCodeUsagesString() { + return "Found reasons to migrate"; + } + + public void showRootUsages(UsageInfo root, UsageInfo migration, final TypeMigrationLabeler labeler) { + final PsiElement rootElement = root.getElement(); + if (rootElement == null) return; + final UsageInfoToUsageConverter.TargetElementsDescriptor targetElementsDescriptor = + new UsageInfoToUsageConverter.TargetElementsDescriptor(rootElement); + final Set usages = labeler.getTypeUsages((TypeMigrationUsageInfo)migration, ((TypeMigrationUsageInfo)root)); + if (usages != null) { + final List infos = new ArrayList(usages.size()); + for (PsiElement usage : usages) { + if (usage != null && usage.isValid()) { + infos.add(new UsageInfo(usage)); + } + } + showUsages(targetElementsDescriptor, infos.toArray(new UsageInfo[infos.size()])); + } else { + showUsages(targetElementsDescriptor, new UsageInfo[] {migration}); + } + } + + @Override + public Dimension getMaximumSize() { + return new Dimension(-1, 300); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationDialog.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationDialog.java new file mode 100644 index 000000000000..17528ab59400 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationDialog.java @@ -0,0 +1,230 @@ +/* + * User: anna + * Date: 25-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.find.FindSettings; +import com.intellij.ide.util.scopeChooser.ScopeChooserCombo; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.fileTypes.StdFileTypes; +import com.intellij.openapi.help.HelpManager; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.LabeledComponent; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; +import com.intellij.refactoring.typeMigration.TypeMigrationProcessor; +import com.intellij.refactoring.typeMigration.TypeMigrationRules; +import com.intellij.refactoring.ui.RefactoringDialog; +import com.intellij.refactoring.ui.TypeSelectorManagerImpl; +import com.intellij.ui.EditorComboBox; +import com.intellij.util.VisibilityUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; + +public class TypeMigrationDialog extends RefactoringDialog { + public static final String REFACTORING_NAME = "Type Migration"; + + private final EditorComboBox myToTypeEditor; + private final PsiElement myRoot; + private TypeMigrationRules myRules; + private final PsiTypeCodeFragment myTypeCodeFragment; + + private final ScopeChooserCombo myScopeChooserCombo; + + public PsiElement getRoot() { + return myRoot; + } + + @Nullable + public PsiType getMigrationType() { + try { + return myTypeCodeFragment.getType(); + } + catch (PsiTypeCodeFragment.TypeSyntaxException e) { + return null; + } + catch (PsiTypeCodeFragment.NoTypeException e) { + return null; + } + } + + public TypeMigrationDialog(@NotNull Project project, PsiElement root, TypeMigrationRules rules) { + super(project, false); + myRoot = root; + myRules = rules; + final PsiType migrationRootType = rules != null ? rules.getMigrationRootType() : null; + myTypeCodeFragment = JavaPsiFacade.getInstance(project).getElementFactory().createTypeCodeFragment( + migrationRootType != null ? migrationRootType.getPresentableText() : getRootType().getPresentableText(), root, false, true, + root instanceof PsiParameter && ((PsiParameter)root).getDeclarationScope() instanceof PsiMethod); + final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + final Document document = documentManager.getDocument(myTypeCodeFragment); + assert document != null; + myToTypeEditor = new EditorComboBox(document, project, StdFileTypes.JAVA); + final String[] types = getValidTypes(project, root); + if (types != null) { + myToTypeEditor.setHistory(types); + } else { + myToTypeEditor.setHistory(new String[]{document.getText()}); + } + document.addDocumentListener(new DocumentAdapter() { + @Override + public void documentChanged(final DocumentEvent e) { + documentManager.commitDocument(document); + validateButtons(); + } + }); + + myScopeChooserCombo = new ScopeChooserCombo(project, false, true, FindSettings.getInstance().getDefaultScopeName()); + Disposer.register(myDisposable, myScopeChooserCombo); + myScopeChooserCombo.getChildComponent().addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + validateButtons(); + } + }); + init(); + setTitle(REFACTORING_NAME); + } + + @Nullable + private String[] getValidTypes(final Project project, final PsiElement root) { + final ArrayList expressions = new ArrayList(); + if (root instanceof PsiField || root instanceof PsiMethod) { + if (VisibilityUtil.compare(VisibilityUtil.getVisibilityModifier(((PsiModifierListOwner)root).getModifierList()), PsiModifier.PRIVATE) < 0) return null; + } + + for (PsiReference reference : ReferencesSearch.search(root, GlobalSearchScope.fileScope(root.getContainingFile()))) { + final PsiElement element = reference.getElement(); + final PsiExpression expr = PsiTreeUtil.getParentOfType(element, PsiExpression.class, false); + if (expr != null) { + expressions.add(expr); + } + } + try { + final PsiType[] psiTypes = new TypeSelectorManagerImpl(project, myTypeCodeFragment.getType(), expressions.toArray(new PsiExpression[expressions.size()])).getTypesForAll(); + if (psiTypes.length > 0) { + final String[] history = new String[psiTypes.length]; + for (int i = 0; i < psiTypes.length; i++) { + PsiType psiType = psiTypes[i]; + history[i] = psiType.getCanonicalText(); + } + return history; + } + } + catch (PsiTypeCodeFragment.TypeSyntaxException e) { + return null; + } + catch (PsiTypeCodeFragment.NoTypeException e) { + return null; + } + return null; + } + + @Override + protected void canRun() throws ConfigurationException { + if (!checkType(getMigrationType())) throw new ConfigurationException("\'" + StringUtil.first(myTypeCodeFragment.getText(), 10, true) + "\' is invalid type"); + if (myScopeChooserCombo.getSelectedScope() == null) throw new ConfigurationException("Scope is not chosen"); + } + + private static boolean checkType(final PsiType type) { + if (type == null) return false; + if (!type.isValid()) return false; + if (type instanceof PsiClassType){ + final PsiClassType psiClassType = (PsiClassType)type; + if (psiClassType.resolve() == null) return false; + final PsiType[] types = psiClassType.getParameters(); + for (PsiType paramType : types) { + if (paramType instanceof PsiPrimitiveType || (paramType instanceof PsiWildcardType && ((PsiWildcardType)paramType).getBound() instanceof PsiPrimitiveType)) return false; + if (!checkType(paramType)) return false; + } + } + if (type instanceof PsiArrayType) { + return checkType(type.getDeepComponentType()); + } + return true; + } + + protected void doAction() { + FindSettings.getInstance().setDefaultScopeName(myScopeChooserCombo.getSelectedScopeName()); + + final PsiType rootType = getRootType(); + final PsiType migrationType = getMigrationType(); + + if (Comparing.equal(rootType, migrationType)){ + close(DialogWrapper.OK_EXIT_CODE); + return; + } + + if (myRules == null) { + myRules = new TypeMigrationRules(rootType); + myRules.setMigrationRootType(migrationType); + myRules.setBoundScope(myScopeChooserCombo.getSelectedScope()); + } + invokeRefactoring(new TypeMigrationProcessor(myProject, myRoot, myRules)); + } + + @Nullable + private PsiType getRootType() { + return TypeMigrationLabeler.getElementType(myRoot); + } + + protected JComponent createCenterPanel() { + final JPanel panel = new JPanel(new GridBagLayout()); + final GridBagConstraints gc = new GridBagConstraints(0, GridBagConstraints.RELATIVE, 1, 1, 1, 0, GridBagConstraints.NORTHWEST, + GridBagConstraints.HORIZONTAL, new Insets(5, 5, 0, 0), 0, 0); + panel.add(new JLabel("Migrate " + getElementPresentation(myRoot) + " \"" + getRootType().getCanonicalText() + "\" to"), gc); + panel.add(myToTypeEditor, gc); + + LabeledComponent scopeChooserComponent = new LabeledComponent(); + scopeChooserComponent.setComponent(myScopeChooserCombo); + scopeChooserComponent.setText("Choose scope where change signature may occur"); + panel.add(scopeChooserComponent, gc); + return panel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myToTypeEditor; + } + + private static String getElementPresentation(PsiElement element) { + if (element instanceof PsiMethod) { + return "return type of method " + ((PsiMethod)element).getName(); + } + + if (element instanceof PsiField) { + return "type of field " + ((PsiField)element).getName(); + } + + if (element instanceof PsiLocalVariable) { + return "type of variable " + ((PsiLocalVariable)element).getName(); + } + + if (element instanceof PsiParameter) { + final PsiParameter parm = (PsiParameter)element; + return "type of parameter " + parm.getName() + " of method " + PsiTreeUtil.getParentOfType(parm, PsiMethod.class).getName(); + } + + return element.toString(); + } + + protected void doHelpAction() { + HelpManager.getInstance().invokeHelp("reference.typeMigrationDialog"); + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeBuilder.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeBuilder.java new file mode 100644 index 000000000000..04cd9ee018b1 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeBuilder.java @@ -0,0 +1,32 @@ +/* + * User: anna + * Date: 11-Apr-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.ide.util.treeView.AbstractTreeBuilder; +import com.intellij.ide.util.treeView.AlphaComparator; +import com.intellij.ide.util.treeView.NodeDescriptor; +import com.intellij.openapi.project.Project; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeModel; + +public class TypeMigrationTreeBuilder extends AbstractTreeBuilder{ + public TypeMigrationTreeBuilder(JTree tree, Project project) { + super(tree, (DefaultTreeModel)tree.getModel(), new TypeMigrationTreeStructure(project), AlphaComparator.INSTANCE, false); + initRootNode(); + } + + protected boolean isAlwaysShowPlus(final NodeDescriptor nodeDescriptor) { + return false; + } + + protected boolean isAutoExpandNode(final NodeDescriptor nodeDescriptor) { + return false; + } + + public void setRoot(MigrationRootNode root) { + ((TypeMigrationTreeStructure)getTreeStructure()).setRoot(root); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeStructure.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeStructure.java new file mode 100644 index 000000000000..d34d27899117 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/ui/TypeMigrationTreeStructure.java @@ -0,0 +1,45 @@ +/* + * User: anna + * Date: 11-Apr-2008 + */ +package com.intellij.refactoring.typeMigration.ui; + +import com.intellij.ide.projectView.TreeStructureProvider; +import com.intellij.ide.util.treeView.AbstractTreeStructureBase; +import com.intellij.openapi.project.Project; + +import java.util.Collections; +import java.util.List; + +public class TypeMigrationTreeStructure extends AbstractTreeStructureBase { + private MigrationRootNode myRoot; + + public TypeMigrationTreeStructure(final Project project) { + super(project); + } + + public void setRoot(final MigrationRootNode root) { + myRoot = root; + } + + public List getProviders() { + return Collections.emptyList(); + } + + public Object getRootElement() { + return myRoot; + } + + public void commit() { + + } + + public boolean hasSomethingToCommit() { + return false; + } + + @Override + public boolean isToBuildChildrenInBackground(final Object element) { + return true; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverridenUsageInfo.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverridenUsageInfo.java new file mode 100644 index 000000000000..f7c3cb064903 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverridenUsageInfo.java @@ -0,0 +1,24 @@ +/* + * User: anna + * Date: 27-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.usageInfo; + +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +public class OverridenUsageInfo extends TypeMigrationUsageInfo { + private OverriderUsageInfo[] myOverriders; + + public OverridenUsageInfo(@NotNull PsiElement element) { + super(element); + } + + public OverriderUsageInfo[] getOverridingElements() { + return myOverriders; + } + + public void setOverriders(final OverriderUsageInfo[] overriders) { + myOverriders = overriders; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverriderUsageInfo.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverriderUsageInfo.java new file mode 100644 index 000000000000..9a4a19cb149c --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/OverriderUsageInfo.java @@ -0,0 +1,22 @@ +/* + * User: anna + * Date: 27-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.usageInfo; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import org.jetbrains.annotations.NotNull; + +public class OverriderUsageInfo extends TypeMigrationUsageInfo{ + private final PsiMethod myBaseMethod; + + public OverriderUsageInfo(@NotNull PsiElement element, PsiMethod baseMethod) { + super(element); + myBaseMethod = baseMethod; + } + + public PsiMethod getBaseMethod() { + return myBaseMethod; + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/TypeMigrationUsageInfo.java b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/TypeMigrationUsageInfo.java new file mode 100644 index 000000000000..dc032be05368 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/typeMigration/usageInfo/TypeMigrationUsageInfo.java @@ -0,0 +1,41 @@ +/* + * User: anna + * Date: 27-Mar-2008 + */ +package com.intellij.refactoring.typeMigration.usageInfo; + +import com.intellij.openapi.util.Comparing; +import com.intellij.psi.PsiElement; +import com.intellij.usageView.UsageInfo; +import org.jetbrains.annotations.NotNull; + +public class TypeMigrationUsageInfo extends UsageInfo { + private boolean myExcluded; + + + public TypeMigrationUsageInfo(@NotNull PsiElement element) { + super(element); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof TypeMigrationUsageInfo)) return false; + + final UsageInfo usageInfo = (UsageInfo)o; + + if (endOffset != usageInfo.endOffset) return false; + if (isNonCodeUsage != usageInfo.isNonCodeUsage) return false; + if (startOffset != usageInfo.startOffset) return false; + return Comparing.equal(getElement(), usageInfo.getElement()); + } + + public void setExcluded(final boolean excluded) { + myExcluded = excluded; + } + + public boolean isExcluded() { + return myExcluded; + } + +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/util/EnumConstantsUtil.java b/java/java-impl/src/com/intellij/refactoring/util/EnumConstantsUtil.java index ab27910b7288..70a747e701d3 100644 --- a/java/java-impl/src/com/intellij/refactoring/util/EnumConstantsUtil.java +++ b/java/java-impl/src/com/intellij/refactoring/util/EnumConstantsUtil.java @@ -23,6 +23,9 @@ package com.intellij.refactoring.util; import com.intellij.psi.*; import com.intellij.psi.util.TypeConversionUtil; import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; public class EnumConstantsUtil { private EnumConstantsUtil() { @@ -48,4 +51,31 @@ public class EnumConstantsUtil { public static PsiEnumConstant createEnumConstant(PsiClass enumClass, PsiLocalVariable local, final String fieldName) throws IncorrectOperationException { return createEnumConstant(enumClass, fieldName, local.getInitializer()); } + + @Nullable + public static PsiStatement isEnumSwitch(final PsiSwitchStatement switchStatement, + final PsiType enumValueType, + final Set enumValues) { + final PsiExpression expression = switchStatement.getExpression(); + if (expression != null) { + final PsiType expressionType = expression.getType(); + if (expressionType != null && !TypeConversionUtil.isAssignable(expressionType, enumValueType)) { + return switchStatement; + } + final PsiConstantEvaluationHelper evaluationHelper = + JavaPsiFacade.getInstance(expression.getProject()).getConstantEvaluationHelper(); + final PsiCodeBlock body = switchStatement.getBody(); + if (body != null) { + for (PsiStatement statement : body.getStatements()) { + if (statement instanceof PsiSwitchLabelStatement) { + final PsiSwitchLabelStatement labelStatement = (PsiSwitchLabelStatement)statement; + final Object caseValue = evaluationHelper.computeConstantExpression(labelStatement.getCaseValue()); + if (caseValue != null && !enumValues.contains(caseValue)) return statement; + } + } + return null; + } + } + return switchStatement; + } } \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/after/Test.java new file mode 100644 index 000000000000..e85a488d1338 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/after/Test.java @@ -0,0 +1,18 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + void foobar() { + int max = Math.max(FOO, BAR); + foo(max); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/before/Test.java new file mode 100644 index 000000000000..e85a488d1338 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter/before/Test.java @@ -0,0 +1,18 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + void foobar() { + int max = Math.max(FOO, BAR); + foo(max); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/after/Test.java new file mode 100644 index 000000000000..457b2d22684d --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/after/Test.java @@ -0,0 +1,20 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + int k = Math.max(i * i, i + i); + switch (k) { + case FOO: + break; + case BAR: + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/before/Test.java new file mode 100644 index 000000000000..457b2d22684d --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeMethodParameter1/before/Test.java @@ -0,0 +1,20 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + int k = Math.max(i * i, i + i); + switch (k) { + case FOO: + break; + case BAR: + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/after/Test.java new file mode 100644 index 000000000000..e77d50e61657 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/after/Test.java @@ -0,0 +1,13 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 2; + + void foo(String[] args) { + switch (args.length) { + case FOO: + break; + case BAR: + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/before/Test.java new file mode 100644 index 000000000000..e77d50e61657 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType/before/Test.java @@ -0,0 +1,13 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 2; + + void foo(String[] args) { + switch (args.length) { + case FOO: + break; + case BAR: + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/after/Test.java new file mode 100644 index 000000000000..28a75d381613 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/after/Test.java @@ -0,0 +1,17 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 2; + + void foo(String[] args) { + switch (boo(args)) { + case FOO: + break; + case BAR: + break; + } + } + + int boo(String[] args) { + return args.length; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/before/Test.java new file mode 100644 index 000000000000..28a75d381613 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/cantChangeReturnType1/before/Test.java @@ -0,0 +1,17 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 2; + + void foo(String[] args) { + switch (boo(args)) { + case FOO: + break; + case BAR: + break; + } + } + + int boo(String[] args) { + return args.length; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/EEnum.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/EEnum.java new file mode 100644 index 000000000000..dc88a3b1afba --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/EEnum.java @@ -0,0 +1,12 @@ +public enum EEnum { + FOO(0), BAR(1); + private int value; + + public int getValue() { + return value; + } + + EEnum(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/Test.java new file mode 100644 index 000000000000..bc0818f0f3d7 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/after/Test.java @@ -0,0 +1,16 @@ +public class Test { + + void foo(EEnum i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + void foobar() { + foo(EEnum.FOO); + foo(EEnum.BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/before/Test.java new file mode 100644 index 000000000000..b9fa7262fb8b --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameter/before/Test.java @@ -0,0 +1,18 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + void foobar() { + foo(FOO); + foo(BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/EEnum.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/EEnum.java new file mode 100644 index 000000000000..dc88a3b1afba --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/EEnum.java @@ -0,0 +1,12 @@ +public enum EEnum { + FOO(0), BAR(1); + private int value; + + public int getValue() { + return value; + } + + EEnum(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/Test.java new file mode 100644 index 000000000000..50b5b59aabe8 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/after/Test.java @@ -0,0 +1,12 @@ +public class Test { + + void foo(EEnum i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + int k = Math.max(i.getValue() * i.getValue(), i.getValue() + i.getValue()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/before/Test.java new file mode 100644 index 000000000000..48c3cea76488 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeMethodParameterAndReplaceOtherUsages/before/Test.java @@ -0,0 +1,14 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + int k = Math.max(i * i, i + i); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/EEnum.java b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/EEnum.java new file mode 100644 index 000000000000..dc88a3b1afba --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/EEnum.java @@ -0,0 +1,12 @@ +public enum EEnum { + FOO(0), BAR(1); + private int value; + + public int getValue() { + return value; + } + + EEnum(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/Test.java new file mode 100644 index 000000000000..3658f1aa724f --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/after/Test.java @@ -0,0 +1,19 @@ +public class Test { + + void foo() { + EEnum i = foobar(false); + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + EEnum foobar(boolean flag) { + if (flag) { + return EEnum.FOO; + } + return EEnum.BAR; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/changeReturnType/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/before/Test.java new file mode 100644 index 000000000000..da43d795d274 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/changeReturnType/before/Test.java @@ -0,0 +1,21 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo() { + int i = foobar(false); + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + int foobar(boolean flag) { + if (flag) { + return FOO; + } + return BAR; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/after/Test.java new file mode 100644 index 000000000000..e73143d8ceb5 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/after/Test.java @@ -0,0 +1,19 @@ +public class Test { + public static final int OK = 0; + public static final int ERROR = 1; + + void foo(int status) { + switch (status) { + case OK: + break; + case ERROR: + break; + case Node.WARNING: + break; + } + } +} + +interface Node { + int WARNING = 2; +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/before/Test.java new file mode 100644 index 000000000000..e73143d8ceb5 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/dontChangeOtherConstants/before/Test.java @@ -0,0 +1,19 @@ +public class Test { + public static final int OK = 0; + public static final int ERROR = 1; + + void foo(int status) { + switch (status) { + case OK: + break; + case ERROR: + break; + case Node.WARNING: + break; + } + } +} + +interface Node { + int WARNING = 2; +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/EEnum.java b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/EEnum.java new file mode 100644 index 000000000000..1a69466c2e3a --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/EEnum.java @@ -0,0 +1,12 @@ +public enum EEnum { + FOO(0), BAR(2); + private int value; + + public int getValue() { + return value; + } + + EEnum(int value) { + this.value = value; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Test.java new file mode 100644 index 000000000000..c38448b383c5 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Test.java @@ -0,0 +1,5 @@ +class Test { + void foo() { + System.out.println(EEnum.FOO.getValue()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Usage.java b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Usage.java new file mode 100644 index 000000000000..310fdee953ef --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/after/Usage.java @@ -0,0 +1,15 @@ +class Usage { + void foo(EEnum i) { + switch (i) { + case FOO: + break; + case BAR: + break; + } + } + + void foobar() { + foo(EEnum.FOO); + foo(EEnum.BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Test.java new file mode 100644 index 000000000000..ac612daf11eb --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Test.java @@ -0,0 +1,7 @@ +class Test { + public static final int FOO = 0; + public static final int BAR = 2; + void foo() { + System.out.println(FOO); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Usage.java b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Usage.java new file mode 100644 index 000000000000..7563c64e1cb5 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/referencesOnEnumConstantElsewhere/before/Usage.java @@ -0,0 +1,15 @@ +class Usage { + void foo(int i) { + switch (i) { + case Test.FOO: + break; + case Test.BAR: + break; + } + } + + void foobar() { + foo(Test.FOO); + foo(Test.BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/after/Test.java b/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/after/Test.java new file mode 100644 index 000000000000..1ee8f0ec6cc9 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/after/Test.java @@ -0,0 +1,20 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + case 8: + break; + } + } + + void foobar() { + foo(FOO); + foo(BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/before/Test.java b/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/before/Test.java new file mode 100644 index 000000000000..1ee8f0ec6cc9 --- /dev/null +++ b/java/java-tests/testData/refactoring/extractEnum/unknownSwitchLabel/before/Test.java @@ -0,0 +1,20 @@ +public class Test { + public static final int FOO = 0; + public static final int BAR = 1; + + void foo(int i) { + switch (i) { + case FOO: + break; + case BAR: + break; + case 8: + break; + } + } + + void foobar() { + foo(FOO); + foo(BAR); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/inlineLocal/Wildcard.java.after b/java/java-tests/testData/refactoring/inlineLocal/Wildcard.java.after index 351037abb958..429e19e7b680 100644 --- a/java/java-tests/testData/refactoring/inlineLocal/Wildcard.java.after +++ b/java/java-tests/testData/refactoring/inlineLocal/Wildcard.java.after @@ -1,4 +1,4 @@ -import java.util.*; +import java.util.Collection; class Test { void foo() { for (Number extension : getExtensions()) { diff --git a/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/A.java b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/A.java new file mode 100644 index 000000000000..e7628d4c0d71 --- /dev/null +++ b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/A.java @@ -0,0 +1,4 @@ +public enum A { + ONE(1); + A(int i){} +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/B.java b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/B.java new file mode 100644 index 000000000000..2ae14a359635 --- /dev/null +++ b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/after/B.java @@ -0,0 +1,17 @@ +public class B { + void foo(A i) { + switch (i) { + case ONE : + break; + } + } +} + +class Usage { + void foo(A i) { + switch (i) { + case ONE : + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/A.java b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/A.java new file mode 100644 index 000000000000..86e963238abe --- /dev/null +++ b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/A.java @@ -0,0 +1,4 @@ +public enum A { + ; + A(int i){} +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/B.java b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/B.java new file mode 100644 index 000000000000..215d0ea0dc95 --- /dev/null +++ b/java/java-tests/testData/refactoring/moveMembers/enumConstantFromCaseStatement/before/B.java @@ -0,0 +1,18 @@ +public class B { + public static final int ONE = 1; + void foo(A i) { + switch (i) { + case ONE : + break; + } + } +} + +class Usage { + void foo(A i) { + switch (i) { + case B.ONE : + break; + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/projectView/BaseProjectViewTestCase.java b/java/java-tests/testSrc/com/intellij/projectView/BaseProjectViewTestCase.java index f994d4fd51cf..eb79e4972970 100644 --- a/java/java-tests/testSrc/com/intellij/projectView/BaseProjectViewTestCase.java +++ b/java/java-tests/testSrc/com/intellij/projectView/BaseProjectViewTestCase.java @@ -15,7 +15,6 @@ */ package com.intellij.projectView; -import com.intellij.JavaTestUtil; import com.intellij.ide.SelectInTarget; import com.intellij.ide.projectView.BaseProjectTreeBuilder; import com.intellij.ide.projectView.ProjectView; @@ -25,16 +24,17 @@ import com.intellij.ide.projectView.impl.nodes.PackageElementNode; import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode; import com.intellij.ide.util.treeView.*; import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.project.DumbAwareRunnable; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.impl.JavaSdkImpl; -import com.intellij.openapi.ui.Queryable; -import com.intellij.testFramework.IdeaTestUtil; -import com.intellij.openapi.project.DumbAwareRunnable; import com.intellij.openapi.startup.StartupManager; +import com.intellij.openapi.ui.Queryable; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiClass; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; +import com.intellij.testFramework.IdeaTestUtil; import com.intellij.testFramework.ProjectViewTestUtil; import com.intellij.testFramework.TestSourceBasedTestCase; import com.intellij.util.Function; @@ -47,6 +47,7 @@ import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -290,4 +291,12 @@ public abstract class BaseProjectViewTestCase extends TestSourceBasedTestCase { protected Sdk getTestProjectJdk() { return JavaSdkImpl.getMockJdkCE(); } + + protected static void sortClasses(final PsiClass[] classes) { + Arrays.sort(classes, new Comparator() { + public int compare(PsiClass o1, PsiClass o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } } diff --git a/java/java-tests/testSrc/com/intellij/projectView/NavigateFromSourceTest.java b/java/java-tests/testSrc/com/intellij/projectView/NavigateFromSourceTest.java index 04748a6106bb..c8c0899c4dbc 100644 --- a/java/java-tests/testSrc/com/intellij/projectView/NavigateFromSourceTest.java +++ b/java/java-tests/testSrc/com/intellij/projectView/NavigateFromSourceTest.java @@ -41,12 +41,16 @@ import com.intellij.testFramework.IdeaTestUtil; import javax.swing.*; import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; @SuppressWarnings({"HardCodedStringLiteral"}) public class NavigateFromSourceTest extends BaseProjectViewTestCase { public void testShowClassMembers() throws Exception { useStandardProviders(); - PsiClass psiClass = JavaDirectoryService.getInstance().getClasses(getPackageDirectory())[0]; + final PsiClass[] classes = JavaDirectoryService.getInstance().getClasses(getPackageDirectory()); + sortClasses(classes); + PsiClass psiClass = classes[0]; final AbstractProjectViewPSIPane pane = createPane(); final PsiFile containingFile = psiClass.getContainingFile(); diff --git a/java/java-tests/testSrc/com/intellij/refactoring/ExtractEnumTest.java b/java/java-tests/testSrc/com/intellij/refactoring/ExtractEnumTest.java index 9e143ca9b04d..ca08aac1ae3b 100644 --- a/java/java-tests/testSrc/com/intellij/refactoring/ExtractEnumTest.java +++ b/java/java-tests/testSrc/com/intellij/refactoring/ExtractEnumTest.java @@ -60,7 +60,7 @@ public class ExtractEnumTest extends MultiFileTestCase { } public void testForwardReferenceConflict() throws Exception { - doTest("Enum constant field BAR would forward reference on field field FOO", false, + doTest("Unable to migrate statement to enum constant.", false, new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, false), new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); } @@ -70,6 +70,62 @@ public class ExtractEnumTest extends MultiFileTestCase { new RefactoringTestUtil.MemberDescriptor("value", PsiField.class, false)); } + public void testChangeMethodParameter() throws Exception { + doTest(new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testCantChangeMethodParameter() throws Exception { + doTest("Unable to migrate statement to enum constant.", false, + new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testDontChangeOtherConstants() throws Exception { + doTest("Unable to migrate statement to enum constant. Node.WARNING can not be replaced with enum", false, + new RefactoringTestUtil.MemberDescriptor("OK", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("ERROR", PsiField.class, true)); + } + + public void testCantChangeMethodParameter1() throws Exception { + doTest("Unable to migrate statement to enum constant.", false, + new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testChangeReturnType() throws Exception { + doTest(new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testCantChangeReturnType() throws Exception { + doTest("Unable to migrate statement to enum constant. Field <b><code>length</code></b> is out of project", false, + new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testCantChangeReturnType1() throws Exception { + doTest("Unable to migrate statement to enum constant.", false, + new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testChangeMethodParameterAndReplaceOtherUsages() throws Exception { + doTest(new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testReferencesOnEnumConstantElsewhere() throws Exception { + doTest(new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + + public void testUnknownSwitchLabel() throws Exception { + doTest("Unable to migrate statement to enum constant. 8 can not be replaced with enum", false, + new RefactoringTestUtil.MemberDescriptor("FOO", PsiField.class, true), + new RefactoringTestUtil.MemberDescriptor("BAR", PsiField.class, true)); + } + private void doTest(final RefactoringTestUtil.MemberDescriptor... memberDescriptors) throws Exception { doTest(null, false, memberDescriptors); } diff --git a/java/java-tests/testSrc/com/intellij/refactoring/MoveMembersTest.java b/java/java-tests/testSrc/com/intellij/refactoring/MoveMembersTest.java index cdb47c39bf6f..323b13e8e0ca 100644 --- a/java/java-tests/testSrc/com/intellij/refactoring/MoveMembersTest.java +++ b/java/java-tests/testSrc/com/intellij/refactoring/MoveMembersTest.java @@ -84,6 +84,10 @@ public class MoveMembersTest extends MultiFileTestCase { doTest("B", "A", 0); } + public void testEnumConstantFromCaseStatement() throws Exception { + doTest("B", "A", 0); + } + public void testDependantFields() throws Exception { doTest("B", "A", 0); } diff --git a/license/yourkit-license-redist.txt b/license/yourkit-license-redist.txt new file mode 100644 index 000000000000..77a88b27ff41 --- /dev/null +++ b/license/yourkit-license-redist.txt @@ -0,0 +1,33 @@ +The following files can be redistributable under the license below: + +yjpagent.dll +libyjpagent.so +libyjpagent.jnilib +yjp-controller-api-redist.jar + +------------------------------------------------------------------- + +Copyright (c) 2003-2009, YourKit +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of YourKit nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY YOURKIT "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YOURKIT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/platform/lang-api/src/com/intellij/codeInspection/ProblemsHolder.java b/platform/lang-api/src/com/intellij/codeInspection/ProblemsHolder.java index c01d4dba74c8..0c27dc3f7521 100644 --- a/platform/lang-api/src/com/intellij/codeInspection/ProblemsHolder.java +++ b/platform/lang-api/src/com/intellij/codeInspection/ProblemsHolder.java @@ -124,7 +124,13 @@ public class ProblemsHolder { if (reference instanceof LocalQuickFixProvider) { fixes = ((LocalQuickFixProvider)reference).getQuickFixes(); } + registerProblemForReference(reference, highlightType, descriptionTemplate, fixes); + } + public void registerProblemForReference(@NotNull PsiReference reference, + ProblemHighlightType highlightType, + String descriptionTemplate, + LocalQuickFix... fixes) { registerProblem(myManager.createProblemDescriptor(reference.getElement(), reference.getRangeInElement(), descriptionTemplate, highlightType, myOnTheFly, fixes)); } diff --git a/platform/lang-api/src/com/intellij/openapi/projectRoots/Sdk.java b/platform/lang-api/src/com/intellij/openapi/projectRoots/Sdk.java index 5e996afe125b..64c4e5df2d2c 100644 --- a/platform/lang-api/src/com/intellij/openapi/projectRoots/Sdk.java +++ b/platform/lang-api/src/com/intellij/openapi/projectRoots/Sdk.java @@ -16,6 +16,7 @@ package com.intellij.openapi.projectRoots; import com.intellij.openapi.roots.RootProvider; +import com.intellij.openapi.util.UserDataHolder; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.Nullable; @@ -23,7 +24,7 @@ import org.jetbrains.annotations.Nullable; * @author Eugene Zhuravlev * Date: Sep 23, 2004 */ -public interface Sdk { +public interface Sdk extends UserDataHolder { SdkType getSdkType(); diff --git a/platform/lang-api/src/com/intellij/psi/PsiFile.java b/platform/lang-api/src/com/intellij/psi/PsiFile.java index 72dede6cb7f1..1d6224e66f87 100644 --- a/platform/lang-api/src/com/intellij/psi/PsiFile.java +++ b/platform/lang-api/src/com/intellij/psi/PsiFile.java @@ -77,8 +77,7 @@ public interface PsiFile extends PsiFileSystemItem { * * @return the array of PSI roots, or a single-element array containing this * if the file has only a single language. - * @deprecated Use {@link com.intellij.psi.FileViewProvider#getLanguages()} and then - * {@link com.intellij.psi.FileViewProvider#getPsi(com.intellij.lang.Language)} instead. + * @deprecated Use {@link FileViewProvider#getAllFiles()} instead. */ @NotNull PsiFile[] getPsiRoots(); diff --git a/platform/lang-impl/src/com/intellij/application/options/CodeStyleAbstractPanel.java b/platform/lang-impl/src/com/intellij/application/options/CodeStyleAbstractPanel.java index 2acf0344ce1e..e54cf3425e45 100644 --- a/platform/lang-impl/src/com/intellij/application/options/CodeStyleAbstractPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/CodeStyleAbstractPanel.java @@ -335,4 +335,8 @@ public abstract class CodeStyleAbstractPanel implements Disposable { protected Editor getEditor() { return myEditor; } + + protected CodeStyleSettings getSettings() { + return mySettings; + } } diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleBlankLinesPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleBlankLinesPanel.java index b5c1e73da957..13b29532cf1c 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleBlankLinesPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleBlankLinesPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options.codeStyle; +import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.Extensions; @@ -68,6 +69,11 @@ public class CodeStyleBlankLinesPanel extends MultilanguageCodeStyleAbstractPane return LanguageCodeStyleSettingsProvider.SettingsType.BLANK_LINE_SETTINGS; } + @Override + protected void onLanguageChange(Language language) { + // TODO: IMPLEMENT + } + private JPanel createBlankLinesPanel() { OptionGroup optionGroup = new OptionGroup(ApplicationBundle.message("title.blank.lines")); diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleIndentAndBracesPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleIndentAndBracesPanel.java index 23ee8e7f4693..9fbcababcf0f 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleIndentAndBracesPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleIndentAndBracesPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options.codeStyle; +import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; @@ -133,6 +134,11 @@ public class CodeStyleIndentAndBracesPanel extends MultilanguageCodeStyleAbstrac return LanguageCodeStyleSettingsProvider.SettingsType.INDENT_AND_BRACES_SETTINGS; } + @Override + protected void onLanguageChange(Language language) { + // TODO: IMPLEMENT + } + private Component createKeepWhenReformattingPanel() { OptionGroup optionGroup = new OptionGroup(ApplicationBundle.message("title.keep.when.reformatting")); diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleSpacesPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleSpacesPanel.java index abfd213b5cc4..9c2b90164ed3 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleSpacesPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/CodeStyleSpacesPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options.codeStyle; +import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.Trinity; @@ -33,6 +34,7 @@ public class CodeStyleSpacesPanel extends OptionTreeWithPreviewPanel implements private boolean myShowAllStandardOptions = false; private Set myAllowedOptions; private MultiMap, String, String>> myCustomOptions; + private boolean myUpdateOnly = false; public CodeStyleSpacesPanel(CodeStyleSettings settings) { super(settings); @@ -43,6 +45,17 @@ public class CodeStyleSpacesPanel extends OptionTreeWithPreviewPanel implements return LanguageCodeStyleSettingsProvider.SettingsType.SPACING_SETTINGS; } + @Override + protected void onLanguageChange(Language language) { + myUpdateOnly = true; + for(LanguageCodeStyleSettingsProvider provider: Extensions.getExtensions(LanguageCodeStyleSettingsProvider.EP_NAME)) { + if (provider.getLanguage().is(language)) { + provider.customizeSpacingOptions(this); + } + } + updateOptionsTree(); + } + protected void initTables() { myAllowedOptions = new HashSet(); myCustomOptions = new MultiMap, String, String>>(); @@ -152,17 +165,25 @@ public class CodeStyleSpacesPanel extends OptionTreeWithPreviewPanel implements public void showAllStandardOptions() { myShowAllStandardOptions = true; + updateOptions(true); } public void showStandardOptions(String... optionNames) { - Collections.addAll(myAllowedOptions, optionNames); + if (!myUpdateOnly) { + Collections.addAll(myAllowedOptions, optionNames); + } + updateOptions(false, optionNames); } public void showCustomOption(Class settingsClass, String fieldName, String optionName, String groupName) { - myCustomOptions.putValue(groupName, - Trinity., String, String>create(settingsClass, fieldName, optionName)); + if (!myUpdateOnly) { + myCustomOptions.putValue(groupName, + Trinity., String, String>create(settingsClass, fieldName, + optionName)); + } + enableOption(fieldName); } } diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/MultilanguageCodeStyleAbstractPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/MultilanguageCodeStyleAbstractPanel.java index b9aea2ef1c01..aae9e0cb11fd 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/MultilanguageCodeStyleAbstractPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/MultilanguageCodeStyleAbstractPanel.java @@ -71,6 +71,7 @@ public abstract class MultilanguageCodeStyleAbstractPanel extends CodeStyleAbstr public void setPanelLanguage(Language language) { myLanguage = language; updatePreviewEditor(); + onLanguageChange(language); } protected abstract LanguageCodeStyleSettingsProvider.SettingsType getSettingsType(); @@ -221,5 +222,6 @@ public abstract class MultilanguageCodeStyleAbstractPanel extends CodeStyleAbstr return new JLabel(""); } + protected abstract void onLanguageChange(Language language); } diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/OptionTreeWithPreviewPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/OptionTreeWithPreviewPanel.java index d4a07cfb0f04..7e842b7d01bf 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/OptionTreeWithPreviewPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/OptionTreeWithPreviewPanel.java @@ -66,6 +66,40 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA } + protected void updateOptions(boolean showAllStandardOptions, String... allowedOptions) { + for (BooleanOptionKey key : myKeys) { + String fieldName = key.field.getName(); + if (key instanceof CustomBooleanOptionKey) { + key.setEnabled(false); + } + else if (showAllStandardOptions) { + key.setEnabled(true); + } + else { + key.setEnabled(false); + for (String optionName: allowedOptions) { + if (fieldName.equals(optionName)) { + key.setEnabled(true); + break; + } + } + } + } + } + + protected void enableOption(String optionName) { + for (BooleanOptionKey key : myKeys) { + if (key.field.getName().equals(optionName)) { + key.setEnabled(true); + } + } + } + + protected void updateOptionsTree() { + resetImpl(getSettings()); + myOptionsTree.repaint(); + } + protected JTree createOptionsTree() { DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); String groupName = ""; @@ -159,6 +193,7 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA try { BooleanOptionKey key = (BooleanOptionKey)childNode.getKey(); childNode.setSelected(key.getValue(settings)); + childNode.setEnabled(key.isEnabled()); } catch (IllegalArgumentException e) { LOG.error(e); @@ -288,7 +323,7 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA button.setBackground(UIUtil.getTreeTextBackground()); } - button.setEnabled(tree.isEnabled()); + button.setEnabled(tree.isEnabled() && treeNode.isEnabled()); return button; } @@ -363,6 +398,7 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA final String groupName; final String cbName; final Field field; + private boolean enabled = true; public BooleanOptionKey(String groupName, String cbName, Field field) { this.groupName = groupName; @@ -392,6 +428,14 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA public boolean getValue(CodeStyleSettings settings) throws IllegalAccessException { return field.getBoolean(settings); } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return this.enabled; + } } private static class CustomBooleanOptionKey extends BooleanOptionKey { @@ -424,6 +468,7 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA private final Object myKey; private final String myText; private boolean isSelected; + private boolean isEnabled = true; public MyToggleTreeNode(Object key, String text) { myKey = key; @@ -437,6 +482,10 @@ public abstract class OptionTreeWithPreviewPanel extends MultilanguageCodeStyleA public void setSelected(boolean val) { isSelected = val; } public boolean isSelected() { return isSelected; } + + public void setEnabled(boolean val) { isEnabled = val; } + + public boolean isEnabled() { return isEnabled; } } public JComponent getInternalPanel() { diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/WrappingPanel.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/WrappingPanel.java index 7b0bc319f83f..2f479d0a8a9e 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/WrappingPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/WrappingPanel.java @@ -15,6 +15,7 @@ */ package com.intellij.application.options.codeStyle; +import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.psi.codeStyle.CodeStyleSettings; @@ -67,6 +68,11 @@ public class WrappingPanel extends OptionTableWithPreviewPanel { return LanguageCodeStyleSettingsProvider.SettingsType.WRAPPING_SETTINGS; } + @Override + protected void onLanguageChange(Language language) { + //TODO: IMPLEMENT + } + protected void initTables() { initRadioGroupField("EXTENDS_LIST_WRAP", EXTENDS_LIST_WRAPPING, FULL_WRAP_OPTIONS, FULL_WRAP_VALUES); initRadioGroupField("EXTENDS_KEYWORD_WRAP", EXTENDS_KEYWORD_WRAPPING, SINGLE_ITEM_WRAP_OPTIONS, diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java index 2d340bec8c15..08e6ad452259 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java @@ -96,7 +96,6 @@ public class TemplateSettings implements PersistentStateComponent, Expo private static final @NonNls String TEMPLATES_CONFIG_FOLDER = "templates"; - private final List myAllTemplates = new ArrayList(); private final MultiMap myTemplates = new MultiMap(); private final Map myTemplatesById = new LinkedHashMap(); private final Map myDefaultTemplates = new LinkedHashMap(); @@ -303,7 +302,8 @@ public class TemplateSettings implements PersistentStateComponent, Expo } public TemplateImpl[] getTemplates() { - return myAllTemplates.toArray(new TemplateImpl[myAllTemplates.size()]); + final Collection all = myTemplates.values(); + return all.toArray(new TemplateImpl[all.size()]); } public char getDefaultShortcutChar() { @@ -369,7 +369,6 @@ public class TemplateSettings implements PersistentStateComponent, Expo final TemplateImpl templateImpl = (TemplateImpl)template; if (getTemplate(templateImpl.getKey(), templateImpl.getGroupName()) == null) { myTemplates.putValue(template.getKey(), templateImpl); - myAllTemplates.add(templateImpl); } myMaxKeyLength = Math.max(myMaxKeyLength, template.getKey().length()); @@ -390,7 +389,6 @@ public class TemplateSettings implements PersistentStateComponent, Expo myTemplates.removeValue(template.getKey(), (TemplateImpl )template); TemplateImpl templateImpl = (TemplateImpl)template; - myAllTemplates.remove(templateImpl); String groupName = templateImpl.getGroupName(); TemplateGroup group = mySchemesManager.findSchemeByName(groupName); @@ -647,7 +645,6 @@ public class TemplateSettings implements PersistentStateComponent, Expo public void setTemplates(List newGroups) { myTemplates.clear(); - myAllTemplates.clear(); myDeletedTemplates.clear(); for (TemplateImpl template : myDefaultTemplates.values()) { myDeletedTemplates.add(TemplateKey.keyOf(template)); diff --git a/platform/lang-impl/src/com/intellij/codeInspection/actions/CleanupInspectionIntention.java b/platform/lang-impl/src/com/intellij/codeInspection/actions/CleanupInspectionIntention.java index 2c717108ad6b..4d927a7ac3d6 100644 --- a/platform/lang-impl/src/com/intellij/codeInspection/actions/CleanupInspectionIntention.java +++ b/platform/lang-impl/src/com/intellij/codeInspection/actions/CleanupInspectionIntention.java @@ -23,6 +23,9 @@ import com.intellij.codeInspection.*; import com.intellij.codeInspection.ex.*; import com.intellij.codeInspection.reference.RefManagerImpl; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.progress.EmptyProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.impl.ProgressManagerImpl; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -59,12 +62,16 @@ public class CleanupInspectionIntention implements IntentionAction { public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException { if (!CodeInsightUtilBase.preparePsiElementForWrite(file)) return; - final InspectionManagerEx managerEx = ((InspectionManagerEx)InspectionManagerEx.getInstance(project)); + final InspectionManagerEx managerEx = (InspectionManagerEx)InspectionManagerEx.getInstance(project); final GlobalInspectionContextImpl context = managerEx.createNewGlobalContext(false); final LocalInspectionToolWrapper tool = new LocalInspectionToolWrapper(myTool); tool.initialize(context); ((RefManagerImpl)context.getRefManager()).inspectionReadActionStarted(); - tool.processFile(file, true, managerEx, true); + ((ProgressManagerImpl)ProgressManager.getInstance()).executeProcessUnderProgress(new Runnable() { + public void run() { + tool.processFile(file, true, managerEx, true); + } + }, new EmptyProgressIndicator()); final List descriptions = new ArrayList(tool.getProblemDescriptors()); Collections.sort(descriptions, new Comparator() { public int compare(final CommonProblemDescriptor o1, final CommonProblemDescriptor o2) { @@ -76,7 +83,7 @@ public class CleanupInspectionIntention implements IntentionAction { for (CommonProblemDescriptor descriptor : descriptions) { final QuickFix[] fixes = descriptor.getFixes(); if (fixes != null && fixes.length > 0) { - for (QuickFix fix : fixes) { + for (QuickFix fix : fixes) { if (fix != null && fix.getClass().isAssignableFrom(myQuickfixClass)) { final PsiElement element = ((ProblemDescriptor)descriptor).getPsiElement(); if (element != null && element.isValid()) { diff --git a/platform/lang-impl/src/com/intellij/execution/actions/ChooseRunConfigurationAction.java b/platform/lang-impl/src/com/intellij/execution/actions/ChooseRunConfigurationAction.java index 4826fc67de61..7f4c439b930d 100644 --- a/platform/lang-impl/src/com/intellij/execution/actions/ChooseRunConfigurationAction.java +++ b/platform/lang-impl/src/com/intellij/execution/actions/ChooseRunConfigurationAction.java @@ -275,7 +275,7 @@ public class ChooseRunConfigurationAction extends AnAction { execute((ItemWrapper)item, executor); } }); - listPopup.cancel(); + listPopup.closeOk(null); } } } diff --git a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java index eabacdfa1123..e03ad8f52766 100644 --- a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java +++ b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java @@ -202,9 +202,7 @@ public class ShowUsagesAction extends AnAction { final List usages = new ArrayList(); final Set visibleNodes = new LinkedHashSet(); Processor collect = new Processor() { - final UsageTarget[] myUsageTarget = new UsageTarget[] { - new PsiElement2UsageTargetAdapter(handler.getPsiElement()) - }; + final UsageTarget[] myUsageTarget = {new PsiElement2UsageTargetAdapter(handler.getPsiElement())}; public boolean process(@NotNull Usage usage) { synchronized (usages) { @@ -387,7 +385,9 @@ public class ShowUsagesAction extends AnAction { if (!(element instanceof UsageNode)) return element.toString(); UsageNode node = (UsageNode)element; GroupNode group = (GroupNode)node.getParent(); - return node.getUsage().getPresentation().getPlainText() + group.toString(); + Usage usage = node.getUsage(); + if (usage == NullUsage.INSTANCE) return ""; + return usage.getPresentation().getPlainText() + group.toString(); } protected void selectElement(Object element, String selectedText) { diff --git a/platform/lang-impl/src/com/intellij/ide/commander/CommanderPanel.java b/platform/lang-impl/src/com/intellij/ide/commander/CommanderPanel.java index 026f82935b1f..24a3f481ae9f 100644 --- a/platform/lang-impl/src/com/intellij/ide/commander/CommanderPanel.java +++ b/platform/lang-impl/src/com/intellij/ide/commander/CommanderPanel.java @@ -271,6 +271,7 @@ public class CommanderPanel extends JPanel { add(myTitlePanel, BorderLayout.NORTH); final JScrollPane scrollPane = new JScrollPane(myList); + scrollPane.setBorder(null); scrollPane.getVerticalScrollBar().setFocusable(false); // otherwise the scrollbar steals focus and panel switching with tab is broken scrollPane.getHorizontalScrollBar().setFocusable(false); add(scrollPane, BorderLayout.CENTER); diff --git a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ChooseByNameBase.java b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ChooseByNameBase.java index 802acf30fc22..c1b9c98767c1 100644 --- a/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ChooseByNameBase.java +++ b/platform/lang-impl/src/com/intellij/ide/util/gotoByName/ChooseByNameBase.java @@ -17,6 +17,7 @@ package com.intellij.ide.util.gotoByName; import com.intellij.Patches; +import com.intellij.codeInsight.hint.HintManager; import com.intellij.ide.IdeBundle; import com.intellij.ide.actions.CopyReferenceAction; import com.intellij.ide.ui.UISettings; @@ -31,6 +32,8 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.JBPopupListener; +import com.intellij.openapi.ui.popup.LightweightWindowEvent; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Pair; @@ -345,7 +348,17 @@ public abstract class ChooseByNameBase{ public void focusLost(final FocusEvent e) { myHideAlarm.addRequest(new Runnable() { public void run() { - if (!JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent())) { + JBPopup popup = JBPopupFactory.getInstance().getChildFocusedPopup(e.getComponent()); + if (popup != null) { + popup.addListener(new JBPopupListener.Adapter() { + @Override + public void onClosed(LightweightWindowEvent event) { + if (event.isOk()) { + hideHint(); + } + } + }); + } else { hideHint(); } } diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/ProjectJdkImpl.java b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/ProjectJdkImpl.java index 0df9273abb14..93bb832970ba 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/ProjectJdkImpl.java +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/ProjectJdkImpl.java @@ -25,10 +25,7 @@ import com.intellij.openapi.projectRoots.ex.ProjectRootContainer; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.RootProvider; import com.intellij.openapi.roots.impl.RootProviderBaseImpl; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.InvalidDataException; -import com.intellij.openapi.util.JDOMExternalizable; -import com.intellij.openapi.util.WriteExternalException; +import com.intellij.openapi.util.*; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -40,7 +37,7 @@ import org.jetbrains.annotations.NotNull; import java.util.*; -public class ProjectJdkImpl implements JDOMExternalizable, Sdk, SdkModificator { +public class ProjectJdkImpl extends UserDataHolderBase implements JDOMExternalizable, Sdk, SdkModificator { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.impl.ProjectJdkImpl"); private final ProjectRootContainerImpl myRootContainer; private String myName; @@ -205,7 +202,7 @@ public class ProjectJdkImpl implements JDOMExternalizable, Sdk, SdkModificator { } } - public Object clone() throws CloneNotSupportedException { + public Object clone() { ProjectJdkImpl newJdk = new ProjectJdkImpl("", mySdkType); copyTo(newJdk); return newJdk; @@ -295,17 +292,11 @@ public class ProjectJdkImpl implements JDOMExternalizable, Sdk, SdkModificator { // SdkModificator implementation public SdkModificator getSdkModificator() { - try { - ProjectJdkImpl sdk = (ProjectJdkImpl)clone(); - sdk.myOrigin = this; - sdk.myRootContainer.startChange(); - sdk.update(); - return sdk; - } - catch (CloneNotSupportedException e) { - LOG.error(e); // should not happen - return null; - } + ProjectJdkImpl sdk = (ProjectJdkImpl)clone(); + sdk.myOrigin = this; + sdk.myRootContainer.startChange(); + sdk.update(); + return sdk; } public void commitChanges() { diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleSchemesImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleSchemesImpl.java index cce26b611565..a30307a35b8d 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleSchemesImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleSchemesImpl.java @@ -19,11 +19,10 @@ package com.intellij.psi.impl.source.codeStyle; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.ExportableApplicationComponent; import com.intellij.openapi.components.RoamingType; -import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.options.Scheme; import com.intellij.openapi.options.SchemeProcessor; import com.intellij.openapi.options.SchemesManager; import com.intellij.openapi.options.SchemesManagerFactory; -import com.intellij.openapi.options.Scheme; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.DefaultJDOMExternalizer; import com.intellij.openapi.util.InvalidDataException; @@ -47,8 +46,6 @@ import java.util.Collection; * Date: Jul 16, 2002 */ public class CodeStyleSchemesImpl extends CodeStyleSchemes implements ExportableApplicationComponent,JDOMExternalizable { - private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.codeStyle.CodeStyleSchemesImpl"); - @NonNls private static final String DEFAULT_SCHEME_NAME = "Default"; public String CURRENT_SCHEME_NAME = DEFAULT_SCHEME_NAME; @@ -56,7 +53,7 @@ public class CodeStyleSchemesImpl extends CodeStyleSchemes implements Exportable @NonNls private static final String CODESTYLES_DIRECTORY = "codestyles"; private final SchemesManager mySchemesManager; - private static final String FILE_SPEC = "$ROOT_CONFIG$/" + CODESTYLES_DIRECTORY; + @NonNls private static final String FILE_SPEC = "$ROOT_CONFIG$/" + CODESTYLES_DIRECTORY; public CodeStyleSchemesImpl(SchemesManagerFactory schemesManagerFactory) { SchemeProcessor processor = new SchemeProcessor() { diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceUtil.java b/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceUtil.java new file mode 100644 index 000000000000..938d8bed9781 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/resolve/reference/impl/providers/FileReferenceUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.impl.source.resolve.reference.impl.providers; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import org.jetbrains.annotations.Nullable; + +/** + * @author Konstantin Bulenkov + */ +public class FileReferenceUtil { + private FileReferenceUtil() { + } + + /** + * Returns a PsiFile element referenced to + * + * @param element some PsiElement + * @return a PsiFile element referenced to + * @see FileReference + * @see com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet + */ + @Nullable + public static PsiFile findFile(@Nullable PsiElement element) { + return element == null ? null : findFile(element.getReferences()); + } + + /** + * Iterates all references starting from the end and looking for FileReference, + * when returns resolve() on it. + * + * @param references references, typically from PsiElement.getReferences() + * @return PsiFile if the last FileReference resolves into a real file. + * @see com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference + * @see com.intellij.psi.PsiElement#getReferences() + */ + @Nullable + public static PsiFile findFile(PsiReference...references) { + for (int i = references.length - 1; i >= 0; i--) { + if (references[i] instanceof FileReference) { + final PsiElement file = references[i].resolve(); + return file instanceof PsiFile ? (PsiFile)file : null; + } + } + return null; + } +} diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/LazyParseableElement.java b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/LazyParseableElement.java index db35eb1d035c..85e43cb63632 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/tree/LazyParseableElement.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/tree/LazyParseableElement.java @@ -21,6 +21,7 @@ package com.intellij.psi.impl.source.tree; import com.intellij.lang.ASTNode; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.psi.PsiLock; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.ILazyParseableElementType; import com.intellij.util.text.CharArrayUtil; @@ -28,6 +29,12 @@ import org.jetbrains.annotations.NotNull; public class LazyParseableElement extends CompositeElement { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.tree.LazyParseableElement"); + private static final RecursiveTreeElementWalkingVisitor CREATE_PSI = new RecursiveTreeElementWalkingVisitor(false) { + @Override + protected void visitNode(TreeElement element) { + element.getPsi(); + } + }; private static class ChameleonLock { private ChameleonLock() {} @@ -160,6 +167,16 @@ public class LazyParseableElement extends CompositeElement { if (parsedNode != null) { rawAddChildren((TreeElement)parsedNode); + + //ensure PSI is created all at once, to reduce contention of PsiLock in CompositeElement.getPsi() + /* + synchronized (PsiLock.LOCK) { + while (parsedNode != null) { + ((TreeElement)parsedNode).acceptTree(CREATE_PSI); + parsedNode = parsedNode.getTreeNext(); + } + } + */ } } diff --git a/platform/lang-impl/src/com/intellij/refactoring/rename/RenameProcessor.java b/platform/lang-impl/src/com/intellij/refactoring/rename/RenameProcessor.java index aa6f37fbad44..93dd07694ba1 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/rename/RenameProcessor.java +++ b/platform/lang-impl/src/com/intellij/refactoring/rename/RenameProcessor.java @@ -105,9 +105,12 @@ public class RenameProcessor extends BaseRefactoringProcessor { } public void prepareRenaming() { - final RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(myPrimaryElement); - processor.prepareRenaming(myPrimaryElement, myNewName, myAllRenames); - myForceShowPreview = processor.forcesShowPreview(); + final List processors = RenamePsiElementProcessor.allForElement(myPrimaryElement); + myForceShowPreview = false; + for (RenamePsiElementProcessor processor : processors) { + processor.prepareRenaming(myPrimaryElement, myNewName, myAllRenames); + myForceShowPreview |= processor.forcesShowPreview(); + } } @Nullable @@ -286,8 +289,8 @@ public class RenameProcessor extends BaseRefactoringProcessor { String newName = entry.getValue(); final RefactoringElementListener elementListener = getTransaction().getElementListener(element); - RenameUtil.doRename(element, newName, extractUsagesForElement(element, usages), myProject, elementListener); Runnable postRenameCallback = RenamePsiElementProcessor.forElement(element).getPostRenameCallback(element, newName, elementListener); + RenameUtil.doRename(element, newName, extractUsagesForElement(element, usages), myProject, elementListener); if (postRenameCallback != null) { postRenameCallbacks.add(postRenameCallback); } diff --git a/platform/lang-impl/src/com/intellij/refactoring/rename/RenamePsiElementProcessor.java b/platform/lang-impl/src/com/intellij/refactoring/rename/RenamePsiElementProcessor.java index f1d0d0f2bc2b..05260ed2fb84 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/rename/RenamePsiElementProcessor.java +++ b/platform/lang-impl/src/com/intellij/refactoring/rename/RenamePsiElementProcessor.java @@ -34,6 +34,7 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -72,6 +73,16 @@ public abstract class RenamePsiElementProcessor { public void findExistingNameConflicts(final PsiElement element, final String newName, final MultiMap conflicts) { } + public static List allForElement(PsiElement element) { + final List result = new ArrayList(); + for (RenamePsiElementProcessor processor : EP_NAME.getExtensions()) { + if (processor.canProcessElement(element)) { + result.add(processor); + } + } + return result; + } + @NotNull public static RenamePsiElementProcessor forElement(PsiElement element) { for(RenamePsiElementProcessor processor: Extensions.getExtensions(EP_NAME)) { diff --git a/platform/lvcs-impl/testSrc/com/intellij/historyIntegrTests/FileListeningTest.java b/platform/lvcs-impl/testSrc/com/intellij/historyIntegrTests/FileListeningTest.java index 24d4611d518f..f6d45d1831b4 100644 --- a/platform/lvcs-impl/testSrc/com/intellij/historyIntegrTests/FileListeningTest.java +++ b/platform/lvcs-impl/testSrc/com/intellij/historyIntegrTests/FileListeningTest.java @@ -31,10 +31,7 @@ import com.intellij.util.io.ReadOnlyAttributeUtil; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; public class FileListeningTest extends IntegrationTestCase { public void testCreatingFiles() throws Exception { @@ -233,11 +230,13 @@ public class FileListeningTest extends IntegrationTestCase { List changes = getVcs().getChangeListInTests().getChangesInTests().get(0).getChanges(); assertEquals(1, changes.size()); Entry e = ((DeleteChange)changes.get(0)).getDeletedEntry(); - assertEquals(2, e.getChildren().size()); - assertEquals("f.txt", e.getChildren().get(0).getName()); - assertEquals("subdir", e.getChildren().get(1).getName()); - assertEquals(1, e.getChildren().get(1).getChildren().size()); - assertEquals("subdir2", e.getChildren().get(1).getChildren().get(0).getName()); + final List children = e.getChildren(); + sortEntries(children); + assertEquals(2, children.size()); + assertEquals("f.txt", children.get(0).getName()); + assertEquals("subdir", children.get(1).getName()); + assertEquals(1, children.get(1).getChildren().size()); + assertEquals("subdir2", children.get(1).getChildren().get(0).getName()); } public void testCreationAndDeletionOfUnversionedFile() throws IOException { @@ -286,4 +285,11 @@ public class FileListeningTest extends IntegrationTestCase { assertNull(revs.get(3).getEntry().findEntry("dir/subDir")); } + private static void sortEntries(final List entries) { + Collections.sort(entries, new Comparator() { + public int compare(Entry o1, Entry o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } } diff --git a/platform/platform-api/src/com/intellij/ide/GeneralSettings.java b/platform/platform-api/src/com/intellij/ide/GeneralSettings.java index 071e54751b47..2c4b54e6fe14 100644 --- a/platform/platform-api/src/com/intellij/ide/GeneralSettings.java +++ b/platform/platform-api/src/com/intellij/ide/GeneralSettings.java @@ -52,6 +52,7 @@ public class GeneralSettings implements NamedJDOMExternalizable, ExportableAppli private String myLastProjectLocation; private boolean mySearchInBackground; private boolean myConfirmExit = true; + private int myConfirmOpenNewProject = -1; @NonNls private static final String ELEMENT_OPTION = "option"; @NonNls private static final String ATTRIBUTE_NAME = "name"; @NonNls private static final String ATTRIBUTE_VALUE = "value"; @@ -73,6 +74,7 @@ public class GeneralSettings implements NamedJDOMExternalizable, ExportableAppli @NonNls private static final String OPTION_USE_CYCLIC_BUFFER = "useCyclicBuffer"; @NonNls private static final String OPTION_SEARCH_IN_BACKGROUND = "searchInBackground"; @NonNls private static final String OPTION_CONFIRM_EXIT = "confirmExit"; + @NonNls private static final String OPTION_CONFIRM_OPEN_NEW_PROJECT = "confirmOpenNewProject"; @NonNls private static final String OPTION_CYCLIC_BUFFER_SIZE = "cyclicBufferSize"; @NonNls private static final String OPTION_LAST_PROJECT_LOCATION = "lastProjectLocation"; @Deprecated @@ -324,6 +326,15 @@ public class GeneralSettings implements NamedJDOMExternalizable, ExportableAppli } } + if (OPTION_CONFIRM_OPEN_NEW_PROJECT.equals(name)) { + try { + myConfirmOpenNewProject = Integer.valueOf(value).intValue(); + } + catch (Exception ex) { + myConfirmOpenNewProject = -1; + } + } + if (OPTION_LAST_PROJECT_LOCATION.equals(name)) { try { myLastProjectLocation = value; @@ -407,6 +418,11 @@ public class GeneralSettings implements NamedJDOMExternalizable, ExportableAppli optionElement.setAttribute(ATTRIBUTE_VALUE, Boolean.toString(myConfirmExit)); parentNode.addContent(optionElement); + optionElement = new Element(ELEMENT_OPTION); + optionElement.setAttribute(ATTRIBUTE_NAME, OPTION_CONFIRM_OPEN_NEW_PROJECT); + optionElement.setAttribute(ATTRIBUTE_VALUE, Integer.toString(myConfirmOpenNewProject)); + parentNode.addContent(optionElement); + if (myLastProjectLocation != null) { optionElement = new Element(ELEMENT_OPTION); optionElement.setAttribute(ATTRIBUTE_NAME, OPTION_LAST_PROJECT_LOCATION); @@ -458,6 +474,14 @@ public class GeneralSettings implements NamedJDOMExternalizable, ExportableAppli myConfirmExit = confirmExit; } + public int getConfirmOpenNewProject() { + return myConfirmOpenNewProject; + } + + public void setConfirmOpenNewProject(int confirmOpenNewProject) { + myConfirmOpenNewProject = confirmOpenNewProject; + } + public boolean isSearchInBackground() { return mySearchInBackground; } diff --git a/platform/platform-api/src/com/intellij/ide/plugins/PluginManager.java b/platform/platform-api/src/com/intellij/ide/plugins/PluginManager.java index 9e551e859660..b9f7a32d5381 100644 --- a/platform/platform-api/src/com/intellij/ide/plugins/PluginManager.java +++ b/platform/platform-api/src/com/intellij/ide/plugins/PluginManager.java @@ -438,10 +438,17 @@ public class PluginManager { } private static Graph createPluginIdGraph(final Map idToDescriptorMap) { - final PluginId[] ids = idToDescriptorMap.keySet().toArray(new PluginId[idToDescriptorMap.size()]); + final List ids = new ArrayList(idToDescriptorMap.keySet()); + // this magic ensures that the dependent plugins always follow their dependencies in lexicographic order + // needed to make sure that extensions are always in the same order + Collections.sort(ids, new Comparator() { + public int compare(PluginId o1, PluginId o2) { + return o2.getIdString().compareTo(o1.getIdString()); + } + }); return GraphGenerator.create(CachingSemiGraph.create(new GraphGenerator.SemiGraph() { public Collection getNodes() { - return Arrays.asList(ids); + return ids; } public Iterator getIn(PluginId pluginId) { @@ -529,6 +536,7 @@ public class PluginManager { private static void loadDescriptorsFromClassPath(final List result) { try { final Collection urls = getClassLoaderUrls(); + final String platformPrefix = System.getProperty("idea.platform.prefix"); for (URL url : urls) { final String protocol = url.getProtocol(); if ("file".equals(protocol)) { @@ -537,7 +545,6 @@ public class PluginManager { //if (!canonicalPath.startsWith(homePath) || canonicalPath.endsWith(".jar")) continue; //if (!canonicalPath.startsWith(homePath)) continue; - final String platformPrefix = System.getProperty("idea.platform.prefix"); IdeaPluginDescriptorImpl platformPluginDescriptor = null; if (platformPrefix != null) { platformPluginDescriptor = loadDescriptor(file, platformPrefix + "Plugin.xml"); diff --git a/platform/platform-api/src/com/intellij/openapi/options/SchemesManager.java b/platform/platform-api/src/com/intellij/openapi/options/SchemesManager.java index b88d5b91188e..450f5c8a552d 100644 --- a/platform/platform-api/src/com/intellij/openapi/options/SchemesManager.java +++ b/platform/platform-api/src/com/intellij/openapi/options/SchemesManager.java @@ -52,7 +52,7 @@ public interface SchemesManager (8,8,8,8) insets. The subclasses can @@ -287,11 +312,49 @@ public abstract class DialogWrapper { panel.add(helpButton, BorderLayout.WEST); } + panel.add(lrButtonsPanel, BorderLayout.CENTER); + + if (myDoNotAsk != null) { + myCheckBoxDoNotShowDialog = new JCheckBox(myDoNotAsk.getDoNotShowMessage()); + + JComponent southPanel = panel; + + if (!myDoNotAsk.canBeHidden()) { + return southPanel; + } + + final JPanel withCB = addDoNotShowCheckBox(southPanel, myCheckBoxDoNotShowDialog); + myCheckBoxDoNotShowDialog.setSelected(!myDoNotAsk.isToBeShown()); + DialogUtil.registerMnemonic(myCheckBoxDoNotShowDialog, '&'); + + panel = withCB; + } + panel.setBorder(IdeBorderFactory.createEmptyBorder(new Insets(8, 0, 0, 0))); + return panel; } + + protected boolean toBeShown() { + return !myCheckBoxDoNotShowDialog.isSelected(); + } + + public static JPanel addDoNotShowCheckBox(JComponent southPanel, JCheckBox checkBox) { + final JPanel panel = new JPanel(new BorderLayout()); + + JPanel wrapper = new JPanel(new GridBagLayout()); + wrapper.add(checkBox); + + panel.add(wrapper, BorderLayout.WEST); + panel.add(southPanel, BorderLayout.EAST); + checkBox.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20)); + + return panel; + } + + private JPanel createButtons(Action[] actions, List buttons) { JPanel buttonsPanel = new JPanel(new GridLayout(1, actions.length, SystemInfo.isMacOSLeopard ? 0 : 5, 0)); for (final Action action : actions) { @@ -470,11 +533,21 @@ public abstract class DialogWrapper { * Note that the method does nothing if "Cancel" action isn't enabled. */ public void doCancelAction() { + processDoNotAskOnCancel(); + if (getCancelAction().isEnabled()) { close(CANCEL_EXIT_CODE); } } + private void processDoNotAskOnCancel() { + if (myDoNotAsk != null) { + if (myDoNotAsk.shouldSaveOptionsOnCancel() && myDoNotAsk.canBeHidden()) { + myDoNotAsk.setToBeShown(toBeShown(), CANCEL_EXIT_CODE); + } + } + } + /** * You can use this method if you want to know by which event this actions got triggered. It is called only if * the cancel action was triggered by some input event, doCancelAction is called otherwise. @@ -503,11 +576,21 @@ public abstract class DialogWrapper { * Note that the method does nothing if "OK" action isn't enabled. */ protected void doOKAction() { + processDoNotAskOnOk(OK_EXIT_CODE); + if (getOKAction().isEnabled()) { close(OK_EXIT_CODE); } } + protected void processDoNotAskOnOk(int exitCode) { + if (myDoNotAsk != null) { + if (myDoNotAsk.canBeHidden()) { + myDoNotAsk.setToBeShown(toBeShown(), exitCode); + } + } + } + /** * @return whether the native window cross butoon closes the window or not. * true means that cross performs hide or dispose of the dialog. @@ -1242,4 +1325,17 @@ public abstract class DialogWrapper { public final Disposable getDisposable() { return myDisposable; } + + public interface DoNotAskOption { + + boolean isToBeShown(); + + void setToBeShown(boolean value, int exitCode); + + boolean canBeHidden(); + + boolean shouldSaveOptionsOnCancel(); + + String getDoNotShowMessage(); + } } \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/openapi/ui/Messages.java b/platform/platform-api/src/com/intellij/openapi/ui/Messages.java index 0c06cc6571c9..71c391d97f9d 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/Messages.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/Messages.java @@ -122,18 +122,20 @@ public class Messages { * @see #showDialog(Project, String, String, String[], int, Icon) * @see #showDialog(Component, String, String, String[], int, Icon) */ - public static int showDialog(String message, String title, String[] options, int defaultOptionIndex, Icon icon) { + public static int showDialog(String message, String title, String[] options, int defaultOptionIndex, Icon icon, DialogWrapper.DoNotAskOption doNotAskOption) { if (isApplicationInUnitTestOrHeadless()) { return ourTestImplementation.show(message); } else { //what's it? if (application.isUnitTestMode()) throw new RuntimeException(message); - MessageDialog dialog = new MessageDialog(message, title, options, defaultOptionIndex, icon); + MessageDialog dialog = new MessageDialog(message, title, options, defaultOptionIndex, icon, doNotAskOption); dialog.show(); return dialog.getExitCode(); } + } - + public static int showDialog(String message, String title, String[] options, int defaultOptionIndex, Icon icon) { + return showDialog(message, title, options, defaultOptionIndex, icon, null); } /** @@ -488,26 +490,32 @@ public class Messages { public MessageDialog(Project project, String message, String title, String[] options, int defaultOptionIndex, Icon icon) { super(project, false); - _init(title, message, options, defaultOptionIndex, icon); + _init(title, message, options, defaultOptionIndex, icon, null); } public MessageDialog(Component parent, String message, String title, String[] options, int defaultOptionIndex, Icon icon) { super(parent, false); - _init(title, message, options, defaultOptionIndex, icon); + _init(title, message, options, defaultOptionIndex, icon, null); } public MessageDialog(String message, String title, String[] options, int defaultOptionIndex, Icon icon) { super(false); - _init(title, message, options, defaultOptionIndex, icon); + _init(title, message, options, defaultOptionIndex, icon, null); } - private void _init(String title, String message, String[] options, int defaultOptionIndex, Icon icon) { + public MessageDialog(String message, String title, String[] options, int defaultOptionIndex, Icon icon, DoNotAskOption doNotAskOption) { + super(false); + _init(title, message, options, defaultOptionIndex, icon, doNotAskOption); + } + + private void _init(String title, String message, String[] options, int defaultOptionIndex, Icon icon, DoNotAskOption doNotAskOption) { setTitle(title); myMessage = message; myOptions = options; myDefaultOptionIndex = defaultOptionIndex; myIcon = icon; setButtonsAlignment(SwingUtilities.CENTER); + setDoNotAskOption(doNotAskOption); init(); } @@ -518,7 +526,7 @@ public class Messages { final int exitCode = i; actions[i] = new AbstractAction(option) { public void actionPerformed(ActionEvent e) { - close(exitCode); + close(exitCode, true); } }; if (i == myDefaultOptionIndex) { diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/Balloon.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/Balloon.java index d9d57238c64e..555766e1a0dd 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/Balloon.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/Balloon.java @@ -17,12 +17,15 @@ package com.intellij.openapi.ui.popup; import com.intellij.openapi.Disposable; import com.intellij.ui.awt.RelativePoint; +import com.intellij.util.ui.PositionTracker; import javax.swing.*; import java.awt.*; public interface Balloon extends Disposable { + void show(PositionTracker tracker, Position preferredPosition); + void show(RelativePoint target, Position prefferedPosition); void show(JLayeredPane pane); diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopup.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopup.java index 21bf971bc40b..b9dea20e77ee 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopup.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopup.java @@ -101,7 +101,13 @@ public interface JBPopup extends Disposable, LightweightWindow { void showCenteredInCurrentWindow(@NotNull Project project); /** - * Cancels the popup (as if Esc was pressed). + * Hides popup as if Enter was pressed or or any other "accept" action + */ + + void closeOk(@Nullable InputEvent e); + + /** + * Cancels the popup as if Esc was pressed or any other "cancel" action */ void cancel(); diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java index af065cddbc20..92ae7a192e83 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/JBPopupFactory.java @@ -107,12 +107,16 @@ public abstract class JBPopupFactory { public abstract RelativePoint guessBestPopupLocation(JComponent component); public boolean isChildPopupFocused(@Nullable Component parent) { - if (parent == null) return false; + return getChildFocusedPopup(parent) != null; + } + + public JBPopup getChildFocusedPopup(@Nullable Component parent) { + if (parent == null) return null; List popups = getChildPopups(parent); for (JBPopup each : popups) { - if (each.isFocused()) return true; + if (each.isFocused()) return each; } - return false; + return null; } /** diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/LightweightWindowEvent.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/LightweightWindowEvent.java index 3ab784252411..b2ff1cba08a2 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/LightweightWindowEvent.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/LightweightWindowEvent.java @@ -16,10 +16,21 @@ package com.intellij.openapi.ui.popup; public class LightweightWindowEvent { + private final LightweightWindow myWindow; + private boolean myOk; public LightweightWindowEvent(LightweightWindow window) { + this(window, false); + } + + public LightweightWindowEvent(LightweightWindow window, boolean isOk) { myWindow = window; + myOk = isOk; + } + + public boolean isOk() { + return myOk; } public Balloon asBalloon() { diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java index e39dc903ed20..79f95d34bb93 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java @@ -191,7 +191,7 @@ public class PopupChooserBuilder { @Override public void mousePressed(MouseEvent e) { if (UIUtil.isActionClick(e) && !isSelectionButtonDown(e) && !e.isConsumed()) { - closePopup(true, e); + closePopup(true, e, true); } } }); @@ -273,17 +273,21 @@ public class PopupChooserBuilder { if (!shouldPerformAction && myChooserComponent instanceof ListWithFilter) { if (((ListWithFilter)myChooserComponent).resetFilter()) return; } - closePopup(shouldPerformAction, null); + closePopup(shouldPerformAction, null, shouldPerformAction); } }, keyStroke, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); } - private void closePopup(boolean shouldPerformAction, MouseEvent e) { + private void closePopup(boolean shouldPerformAction, MouseEvent e, boolean isOk) { if (shouldPerformAction) { myPopup.setFinalRunnable(myItemChoosenRunnable); } - myPopup.cancel(e); + if (isOk) { + myPopup.closeOk(e); + } else { + myPopup.cancel(e); + } } @NotNull diff --git a/platform/platform-api/src/com/intellij/openapi/wm/StatusBarWidget.java b/platform/platform-api/src/com/intellij/openapi/wm/StatusBarWidget.java index 8e2a987192c5..d8ef0d6893a8 100644 --- a/platform/platform-api/src/com/intellij/openapi/wm/StatusBarWidget.java +++ b/platform/platform-api/src/com/intellij/openapi/wm/StatusBarWidget.java @@ -89,7 +89,7 @@ public interface StatusBarWidget extends Disposable { private static final Color PIXEL = LEFT1_FROM_INACTIVE; private static final Color LEFT1_TO_INACTIVE = new Color(180, 180, 180); - private static final Color SEPARATOR_COLOR = UIUtil.getPanelBackgound().darker().darker().darker(); + private static final Color SEPARATOR_COLOR = UIUtil.getPanelBackgound().darker(); public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) { final Graphics2D g2 = (Graphics2D)g.create(); diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java index 1fe06006a32b..8ec78524078d 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java @@ -137,6 +137,7 @@ public class JBTabsImpl extends JComponent private final TabActionsAutoHideListener myTabActionsAutoHideListener = new TabActionsAutoHideListener(); private IdeGlassPane myGlassPane; @NonNls private static final String LAYOUT_DONE = "Layout.done"; + @NonNls public static final String STRETCHED_BY_WIDTH = "Layout.stretchedByWidth"; private TimedDeadzone.Length myTabActionsMouseDeadzone = TimedDeadzone.DEFAULT; @@ -443,10 +444,11 @@ public class JBTabsImpl extends JComponent public void updateTabActions(final boolean validateNow) { final Ref changed = new Ref(Boolean.FALSE); for (final TabInfo eachInfo : myInfo2Label.keySet()) { - updateTab(new Runnable() { - public void run() { + updateTab(new Computable() { + public Boolean compute() { final boolean changes = myInfo2Label.get(eachInfo).updateTabActions(); changed.set(changed.get().booleanValue() || changes); + return changes; } }, eachInfo); } @@ -898,9 +900,10 @@ public class JBTabsImpl extends JComponent } private void updateIcon(final TabInfo tabInfo) { - updateTab(new Runnable() { - public void run() { + updateTab(new Computable() { + public Boolean compute() { myInfo2Label.get(tabInfo).setIcon(tabInfo.getIcon()); + return true; } }, tabInfo); } @@ -908,19 +911,22 @@ public class JBTabsImpl extends JComponent private void updateColor(final TabInfo tabInfo) { myInfo2Label.get(tabInfo).setInactiveStateImage(null); - updateTab(new Runnable() { - public void run() { + updateTab(new Computable() { + public Boolean compute() { repaint(); + return true; } }, tabInfo); } - private void updateTab(Runnable update, TabInfo info) { + private void updateTab(Computable update, TabInfo info) { final TabLabel label = myInfo2Label.get(info); - update.run(); + Boolean changes = update.compute(); if (label.getRootPane() != null) { if (label.isValid()) { - label.repaint(); + if (changes) { + label.repaint(); + } } else { revalidateAndRepaint(false); @@ -972,11 +978,12 @@ public class JBTabsImpl extends JComponent } private void updateText(final TabInfo tabInfo) { - updateTab(new Runnable() { - public void run() { + updateTab(new Computable() { + public Boolean compute() { final TabLabel label = myInfo2Label.get(tabInfo); label.setText(tabInfo.getColoredText()); label.setToolTipText(tabInfo.getTooltipText()); + return true; } }, tabInfo); } @@ -1361,6 +1368,11 @@ public class JBTabsImpl extends JComponent final TabInfo selected = getSelectedInfo(); + if (selected != null) { + Rectangle compBounds = selected.getComponent().getBounds(); + if (compBounds.contains(clip) && !compBounds.intersects(clip)) return; + } + boolean leftGhostExists = isSingleRow(); boolean rightGhostExists = isSingleRow(); @@ -1370,7 +1382,7 @@ public class JBTabsImpl extends JComponent } - paintNonSelectedTabs(g2d, leftGhostExists); + paintNonSelectedTabs(g2d, leftGhostExists, rightGhostExists); if (isSingleRow() && mySingleRowLayout.myLastSingRowLayout.firstGhostVisible) { paintFirstGhost(g2d); @@ -1491,10 +1503,23 @@ public class JBTabsImpl extends JComponent shape.labelLeftX = shape.labelPath.getX(); shape.labelRightX = shape.labelPath.getX() + shape.labelPath.deltaX(shape.labelPath.getWidth()); - shape.path.moveTo(shape.insets.left, shape.labelBottomY); - shape.path.lineTo(shape.labelLeftX, shape.labelBottomY); - shape.path.lineTo(shape.labelLeftX, shape.labelTopY + shape.labelPath.deltaY(getArcSize())); - shape.path.quadTo(shape.labelLeftX, shape.labelTopY, shape.labelLeftX + shape.labelPath.deltaX(getArcSize()), shape.labelTopY); + Insets border = myBorder.getEffectiveBorder(); + TabInfo selected = getSelectedInfo(); + boolean first = myLastLayoutPass.getPreviousFor(selected) == null; + boolean last = myLastLayoutPass.getNextFor(selected) == null; + + boolean leftEdge = !isSingleRow() && first && border.left == 0; + boolean rightEdge = !isSingleRow() && last && Boolean.TRUE.equals(myInfo2Label.get(selected).getClientProperty(STRETCHED_BY_WIDTH)) && border.right == 0; + + if (leftEdge) { + shape.path.moveTo(shape.insets.left, shape.labelTopY); + shape.path.lineTo(shape.labelRightX - shape.labelPath.deltaX(getArcSize()), shape.labelTopY); + } else { + shape.path.moveTo(shape.insets.left, shape.labelBottomY); + shape.path.lineTo(shape.labelLeftX, shape.labelBottomY); + shape.path.lineTo(shape.labelLeftX, shape.labelTopY + shape.labelPath.deltaY(getArcSize())); + shape.path.quadTo(shape.labelLeftX, shape.labelTopY, shape.labelLeftX + shape.labelPath.deltaX(getArcSize()), shape.labelTopY); + } int lastX = shape.path.getWidth() - shape.path.deltaX(shape.insets.right + 1); @@ -1504,18 +1529,28 @@ public class JBTabsImpl extends JComponent shape.path.lineTo(lastX, shape.labelBottomY); } else { - shape.path.lineTo(shape.labelRightX - shape.path.deltaX(getArcSize()), shape.labelTopY); - shape.path.quadTo(shape.labelRightX, shape.labelTopY, shape.labelRightX, shape.labelTopY + shape.path.deltaY(getArcSize())); - if (myLastLayoutPass.hasCurveSpaceFor(getSelectedInfo())) { + if (rightEdge) { + shape.path.lineTo(shape.labelRightX + 1, shape.labelTopY); + } else { + shape.path.lineTo(shape.labelRightX - shape.path.deltaX(getArcSize()), shape.labelTopY); + shape.path.quadTo(shape.labelRightX, shape.labelTopY, shape.labelRightX, shape.labelTopY + shape.path.deltaY(getArcSize())); + } + if (myLastLayoutPass.hasCurveSpaceFor(selected)) { shape.path.lineTo(shape.labelRightX, shape.labelBottomY - shape.path.deltaY(getArcSize())); shape.path.quadTo(shape.labelRightX, shape.labelBottomY, shape.labelRightX + shape.path.deltaX(getArcSize()), shape.labelBottomY); } else { - shape.path.lineTo(shape.labelRightX, shape.labelBottomY); + if (rightEdge) { + shape.path.lineTo(shape.labelRightX + 1, shape.labelBottomY); + } else { + shape.path.lineTo(shape.labelRightX, shape.labelBottomY); + } } } - shape.path.lineTo(lastX, shape.labelBottomY); + if (!rightEdge) { + shape.path.lineTo(lastX, shape.labelBottomY); + } if (isStealthModeEffective()) { shape.path.closePath(); @@ -1633,7 +1668,7 @@ public class JBTabsImpl extends JComponent return Color.white; } - private void paintNonSelectedTabs(final Graphics2D g2d, final boolean leftGhostExists) { + private void paintNonSelectedTabs(final Graphics2D g2d, final boolean leftGhostExists, final boolean rightGhostExists) { TabInfo selected = getSelectedInfo(); if (myLastPaintedSelection == null || !myLastPaintedSelection.equals(selected)) { List tabs = getTabs(); @@ -1648,14 +1683,14 @@ public class JBTabsImpl extends JComponent if (getSelectedInfo() == each) { continue; } - paintNonSelected(g2d, each, leftGhostExists); + paintNonSelected(g2d, each, leftGhostExists, rightGhostExists); } } myLastPaintedSelection = selected; } - private void paintNonSelected(final Graphics2D g2d, final TabInfo each, final boolean leftGhostExists) { + private void paintNonSelected(final Graphics2D g2d, final TabInfo each, final boolean leftGhostExists, final boolean rightGhostExists) { final TabLabel label = myInfo2Label.get(each); if (label.getBounds().width == 0) return; @@ -1675,7 +1710,7 @@ public class JBTabsImpl extends JComponent img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D imgG2d = img.createGraphics(); imgG2d.addRenderingHints(g2d.getRenderingHints()); - doPaintInactictive(imgG2d, leftGhostExists, label, new Rectangle(imageInsets, 0, label.getWidth(), label.getHeight())); + doPaintInactictive(imgG2d, leftGhostExists, label, new Rectangle(imageInsets, 0, label.getWidth(), label.getHeight()), rightGhostExists); imgG2d.dispose(); } @@ -1683,7 +1718,7 @@ public class JBTabsImpl extends JComponent label.setInactiveStateImage(img); } else { - doPaintInactictive(g2d, leftGhostExists, label, label.getBounds()); + doPaintInactictive(g2d, leftGhostExists, label, label.getBounds(), rightGhostExists); label.setInactiveStateImage(null); } } @@ -1692,7 +1727,7 @@ public class JBTabsImpl extends JComponent return Registry.is("ide.tabbedPane.bufferedPaint") && myUseBufferedPaint; } - private void doPaintInactictive(Graphics2D g2d, boolean leftGhostExists, TabLabel label, Rectangle effectiveBounds) { + private void doPaintInactictive(Graphics2D g2d, boolean leftGhostExists, TabLabel label, Rectangle effectiveBounds, boolean rightGhostExists) { int tabIndex = myVisibleInfos.indexOf(label.getInfo()); final int arc = getArcSize(); @@ -1737,12 +1772,30 @@ public class JBTabsImpl extends JComponent int rigthX = !lastShowing && leftFromSelection ? shape.getMaxX() + shape.deltaX(arc + 1) : shape.getMaxX(); int bottomY = shape.getMaxY() + shape.deltaY(1); - shape.moveTo(leftX, bottomY); - shape.lineTo(leftX, topY + shape.deltaY(arc)); - shape.quadTo(leftX, topY, leftX + shape.deltaX(arc), topY); - shape.lineTo(rigthX - shape.deltaX(arc), topY); - shape.quadTo(rigthX, topY, rigthX, topY + shape.deltaY(arc)); - shape.lineTo(rigthX, bottomY); + Insets border = myBorder.getEffectiveBorder(); + + if (border.left > 0 || leftGhostExists || !firstShowing) { + shape.moveTo(leftX, bottomY); + shape.lineTo(leftX, topY + shape.deltaY(arc)); + shape.quadTo(leftX, topY, leftX + shape.deltaX(arc), topY); + } else { + if (firstShowing) { + shape.moveTo(leftX, topY); + } + } + + boolean rightEdge = false; + if (border.right > 0 || rightGhostExists || !lastShowing || !Boolean.TRUE.equals(label.getClientProperty(STRETCHED_BY_WIDTH))) { + shape.lineTo(rigthX - shape.deltaX(arc), topY); + shape.quadTo(rigthX, topY, rigthX, topY + shape.deltaY(arc)); + shape.lineTo(rigthX, bottomY); + } else { + if (lastShowing) { + shape.lineTo(rigthX + 1, topY); + shape.lineTo(rigthX + 1, bottomY); + rightEdge = true; + } + } if (!isSingleRow()) { final TablePassInfo info = myTableLayout.myLastTableLayout; @@ -1753,7 +1806,9 @@ public class JBTabsImpl extends JComponent } } - shape.closePath(); + if (!rightEdge) { + shape.lineTo(leftX, bottomY); + } g2d.setColor(backgroundColor); g2d.fill(shape.getShape()); @@ -1777,8 +1832,10 @@ public class JBTabsImpl extends JComponent g2d.draw( shape.transformLine(leftX + shape.deltaX(arc + 1), topY + shape.deltaY(1), rigthX - shape.deltaX(arc - 1), topY + shape.deltaY(1))); - g2d.setColor(rightBlockColor); - g2d.draw(shape.transformLine(rigthX - shape.deltaX(1), topY + shape.deltaY(arc - 1), rigthX - shape.deltaX(1), bottomY)); + if (!rightEdge) { + g2d.setColor(rightBlockColor); + g2d.draw(shape.transformLine(rigthX - shape.deltaX(1), topY + shape.deltaY(arc - 1), rigthX - shape.deltaX(1), bottomY)); + } g2d.setColor(boundsColor); g2d.draw(shape.getShape()); @@ -2672,6 +2729,7 @@ public class JBTabsImpl extends JComponent public static void resetLayout(JComponent c) { if (c == null) return; c.putClientProperty(LAYOUT_DONE, null); + c.putClientProperty(STRETCHED_BY_WIDTH, null); } private void applyResetComponents() { diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java index 0dba93296ddd..822814c7b546 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java @@ -53,6 +53,7 @@ public class TabLabel extends JPanel { private BufferedImage myInactiveStateImage; private Rectangle myLastPaintedInactiveImageBounds; + private boolean myStretchedByWidth; public TabLabel(JBTabsImpl tabs, final TabInfo info) { myTabs = tabs; diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java index 6d74616104d4..b710c582b7dc 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java @@ -132,6 +132,8 @@ public class TableLayout extends TabLayout { TabInfo tabInfo = eachRow.myColumns.get(i); final TabLabel label = myTabs.myInfo2Label.get(tabInfo); + label.putClientProperty(JBTabsImpl.STRETCHED_BY_WIDTH, Boolean.valueOf(toAjust)); + int width; if (i < eachRow.myColumns.size() - 1 || !toAjust) { width = label.getPreferredSize().width + deltaToFit; diff --git a/platform/platform-api/src/com/intellij/util/ui/OptionsDialog.java b/platform/platform-api/src/com/intellij/util/ui/OptionsDialog.java index a3fd2f94eb6a..8043cc9874da 100644 --- a/platform/platform-api/src/com/intellij/util/ui/OptionsDialog.java +++ b/platform/platform-api/src/com/intellij/util/ui/OptionsDialog.java @@ -26,83 +26,64 @@ import java.awt.*; * author: lesya */ -public abstract class OptionsDialog extends DialogWrapper { - - protected JCheckBox myCheckBoxDoNotShowDialog; - - protected String getDoNotShowMessage() { - return CommonBundle.message("dialog.options.do.not.show"); - } +public abstract class OptionsDialog extends DialogWrapper { protected final Project myProject; + private class MyDoNotAsk implements DoNotAskOption { + public boolean isToBeShown() { + return OptionsDialog.this.isToBeShown(); + } + + public void setToBeShown(boolean value, int exitCode) { + OptionsDialog.this.setToBeShown(value, DialogWrapper.CANCEL_EXIT_CODE != exitCode); + } + + public boolean canBeHidden() { + return OptionsDialog.this.canBeHidden(); + } + + public boolean shouldSaveOptionsOnCancel() { + return OptionsDialog.this.shouldSaveOptionsOnCancel(); + } + + public String getDoNotShowMessage() { + return CommonBundle.message("dialog.options.do.not.show"); + } + } + protected OptionsDialog(Project project) { super(project, true); myProject = project; + setDoNotAskOption(new MyDoNotAsk()); } protected OptionsDialog(Project project, boolean canBeParent) { super(project, canBeParent); myProject = project; + setDoNotAskOption(new MyDoNotAsk()); } protected OptionsDialog(boolean canBeParent) { super(canBeParent); myProject = null; + setDoNotAskOption(new MyDoNotAsk()); } protected OptionsDialog(Component parent, boolean canBeParent) { super(parent, canBeParent); myProject = null; - } - - protected JComponent createSouthPanel() { - - myCheckBoxDoNotShowDialog = new JCheckBox(getDoNotShowMessage()); - - JComponent southPanel = super.createSouthPanel(); - - if (!canBeHidden()) { - return southPanel; - } - - final JPanel panel = addDoNotShowCheckBox(southPanel, myCheckBoxDoNotShowDialog); - myCheckBoxDoNotShowDialog.setSelected(!isToBeShown()); - DialogUtil.registerMnemonic(myCheckBoxDoNotShowDialog, '&'); - return panel; + setDoNotAskOption(new MyDoNotAsk()); } public static JPanel addDoNotShowCheckBox(JComponent southPanel, JCheckBox checkBox) { - final JPanel panel = new JPanel(new GridBagLayout()); - checkBox.setVerticalAlignment(SwingConstants.BOTTOM); - - panel.add(checkBox, new GridBagConstraints(GridBagConstraints.RELATIVE, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(15, 0, 0, 0), 0, 0)); - panel.add(southPanel, new GridBagConstraints(GridBagConstraints.RELATIVE, 0, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); - checkBox.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20)); - return panel; + return DialogWrapper.addDoNotShowCheckBox(southPanel, checkBox); } public static boolean shiftIsPressed(int inputEventModifiers) { return (inputEventModifiers & Event.SHIFT_MASK) != 0; } - protected void doOKAction() { - if (canBeHidden()) { - setToBeShown(toBeShown(), true); - } - super.doOKAction(); - } - - protected boolean toBeShown() { - return !myCheckBoxDoNotShowDialog.isSelected(); - } - - public void doCancelAction() { - if (shouldSaveOptionsOnCancel() && canBeHidden()) { - setToBeShown(toBeShown(), false); - } - super.doCancelAction(); - } protected abstract boolean isToBeShown(); diff --git a/platform/platform-api/src/com/intellij/util/ui/PositionTracker.java b/platform/platform-api/src/com/intellij/util/ui/PositionTracker.java new file mode 100644 index 000000000000..5d7c0b6a1eff --- /dev/null +++ b/platform/platform-api/src/com/intellij/util/ui/PositionTracker.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util.ui; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.util.Disposer; +import com.intellij.ui.awt.RelativePoint; + +import java.awt.*; +import java.awt.event.*; + +public abstract class PositionTracker implements Disposable, HierarchyBoundsListener, HierarchyListener, ComponentListener { + + private Component myComponent; + private Client myClient; + + public PositionTracker(Component component) { + myComponent = component; + } + + public final void init(Client client) { + myClient = client; + + Disposer.register(client, this); + + myComponent.addHierarchyBoundsListener(this); + myComponent.addHierarchyListener(this); + myComponent.addComponentListener(this); + } + + public final Component getComponent() { + return myComponent; + } + + public final void ancestorMoved(HierarchyEvent e) { + revalidate(); + } + + public final void ancestorResized(HierarchyEvent e) { + revalidate(); + } + + public final void hierarchyChanged(HierarchyEvent e) { + revalidate(); + } + + public void componentResized(ComponentEvent e) { + revalidate(); + } + + public void componentMoved(ComponentEvent e) { + revalidate(); + } + + public void componentShown(ComponentEvent e) { + revalidate(); + } + + public void componentHidden(ComponentEvent e) { + revalidate(); + } + + protected final void revalidate() { + myClient.revalidate(this); + } + + public abstract RelativePoint recalculateLocation(T object); + + public final void dispose() { + myComponent.removeHierarchyBoundsListener(this); + myComponent.removeHierarchyListener(this); + myComponent.removeComponentListener(this); + } + + public static final class Static extends PositionTracker { + + private RelativePoint myPoint; + + public Static(RelativePoint point) { + super(point.getComponent()); + myPoint = point; + } + + @Override + public RelativePoint recalculateLocation(Object object) { + return myPoint; + } + } + + public interface Client extends Disposable { + + void revalidate(PositionTracker tracker); + + } + +} diff --git a/platform/platform-impl/src/com/intellij/concurrency/JobUtil.java b/platform/platform-impl/src/com/intellij/concurrency/JobUtil.java index c5f68591b4c4..8f99cb76b59f 100644 --- a/platform/platform-impl/src/com/intellij/concurrency/JobUtil.java +++ b/platform/platform-impl/src/com/intellij/concurrency/JobUtil.java @@ -114,6 +114,14 @@ public class JobUtil { }, failFastOnAcquireReadAction); } + public static void invokeConcurrentlyOnAllCores(@NotNull final Runnable action) throws Throwable { + Job job = new JobImpl(Job.DEFAULT_PRIORITY, false); + for (int i=0; i< JobSchedulerImpl.CORES_COUNT; i++) { + job.addTask(action); + } + job.scheduleAndWaitForResults(); + } + public static Job submitToJobThread(@NotNull final Runnable action, int priority) { Job job = new JobImpl(priority, false); Callable callable = new Callable() { diff --git a/platform/platform-impl/src/com/intellij/ide/GeneralSettingsConfigurable.java b/platform/platform-impl/src/com/intellij/ide/GeneralSettingsConfigurable.java index 4327ecd0b70a..fa9a74fd4a76 100644 --- a/platform/platform-impl/src/com/intellij/ide/GeneralSettingsConfigurable.java +++ b/platform/platform-impl/src/com/intellij/ide/GeneralSettingsConfigurable.java @@ -46,6 +46,9 @@ public class GeneralSettingsConfigurable extends CompositeConfigurable= 0) || (!myComponent.myConfirmFrameToOpenCheckBox.isSelected() == openProjectOption < 0); + int inactiveTimeout = -1; try { inactiveTimeout = Integer.parseInt(myComponent.myTfInactiveTimeout.getText()); @@ -122,6 +129,7 @@ public class GeneralSettingsConfigurable extends CompositeConfigurable - + @@ -33,7 +33,7 @@ - + @@ -45,6 +45,14 @@ + + + + + + + + diff --git a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java index bbae5258b73a..9ab29027e5d5 100644 --- a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java +++ b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java @@ -29,6 +29,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ex.ProjectEx; import com.intellij.openapi.project.ex.ProjectManagerEx; +import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Disposer; @@ -140,9 +141,35 @@ public class ProjectUtil { } if (!forceOpenInNewFrame && openProjects.length > 0) { - int exitCode = Messages.showDialog(IdeBundle.message("prompt.open.project.in.new.frame"), IdeBundle.message("title.open.project"), - new String[]{IdeBundle.message("button.newframe"), IdeBundle.message("button.existingframe"), - CommonBundle.getCancelButtonText()}, 1, Messages.getQuestionIcon()); + final GeneralSettings settings = GeneralSettings.getInstance(); + int exitCode; + if (settings.getConfirmOpenNewProject() < 0) { + exitCode = Messages.showDialog(IdeBundle.message("prompt.open.project.in.new.frame"), IdeBundle.message("title.open.project"), + new String[]{IdeBundle.message("button.newframe"), IdeBundle.message("button.existingframe"), + CommonBundle.getCancelButtonText()}, 1, Messages.getQuestionIcon(), new DialogWrapper.DoNotAskOption() { + public boolean isToBeShown() { + return true; + } + + public void setToBeShown(boolean value, int exitCode) { + settings.setConfirmOpenNewProject(value || exitCode == 2 ? -1 : exitCode); + } + + public boolean canBeHidden() { + return true; + } + + public boolean shouldSaveOptionsOnCancel() { + return false; + } + + public String getDoNotShowMessage() { + return CommonBundle.message("dialog.options.do.not.ask"); + } + }); + } else { + exitCode = settings.getConfirmOpenNewProject(); + } if (exitCode == 1) { // "No" option if (!closeProject(projectToClose != null ? projectToClose : openProjects[openProjects.length - 1])) return null; } diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java index 1c10a94fd5b4..f97c049db6c5 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java @@ -19,6 +19,7 @@ import com.intellij.ide.IdeEventQueue; import com.intellij.ide.actions.CloseAction; import com.intellij.ide.actions.ShowFilePathAction; import com.intellij.ide.ui.UISettings; +import com.intellij.ide.ui.UISettingsListener; import com.intellij.ide.ui.customization.CustomActionsSchema; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; @@ -33,9 +34,7 @@ import com.intellij.openapi.ui.Queryable; import com.intellij.openapi.ui.ShadowAction; import com.intellij.openapi.util.*; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.openapi.wm.ToolWindowAnchor; -import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.openapi.wm.*; import com.intellij.openapi.wm.ex.ToolWindowManagerAdapter; import com.intellij.openapi.wm.ex.ToolWindowManagerEx; import com.intellij.ui.SimpleTextAttributes; @@ -116,6 +115,12 @@ final class EditorTabbedContainer implements Disposable, CloseAction.CloseTarget } }); + UISettings.getInstance().addUISettingsListener(new UISettingsListener() { + public void uiSettingsChanged(UISettings source) { + updateTabBorder(); + } + }); + Disposer.register(project, this); } @@ -134,9 +139,57 @@ final class EditorTabbedContainer implements Disposable, CloseAction.CloseTarget private void updateTabBorder() { if (!myProject.isOpen()) return; + ToolWindowManagerEx mgr = (ToolWindowManagerEx)ToolWindowManager.getInstance(myProject); + + String[] ids = mgr.getToolWindowIds(); + + Insets border = new Insets(0, 0, 0, 0); + + UISettings uiSettings = UISettings.getInstance(); + + for (String each : ids) { + ToolWindow eachWnd = mgr.getToolWindow(each); + if (!eachWnd.isAvailable()) continue; + + if (eachWnd.isVisible() && eachWnd.getType() == ToolWindowType.DOCKED) { + ToolWindowAnchor eachAnchor = eachWnd.getAnchor(); + if (eachAnchor == ToolWindowAnchor.TOP) { + border.top = 1; + } else if (eachAnchor == ToolWindowAnchor.BOTTOM) { + border.bottom = 1; + } else if (eachAnchor == ToolWindowAnchor.LEFT) { + border.left = 1; + } else if (eachAnchor == ToolWindowAnchor.RIGHT) { + border.right = 1; + } + } + } + + if (!uiSettings.HIDE_TOOL_STRIPES) { + if (mgr.getIdsOn(ToolWindowAnchor.TOP).size() > 0) { + border.top = 1; + } + + if (mgr.getIdsOn(ToolWindowAnchor.BOTTOM).size() > 0) { + border.bottom = 1; + } + + if (mgr.getIdsOn(ToolWindowAnchor.LEFT).size() > 0) { + border.left = 1; + } + + if (mgr.getIdsOn(ToolWindowAnchor.RIGHT).size() > 0) { + border.right = 1; + } + + if (!uiSettings.SHOW_STATUS_BAR && !uiSettings.HIDE_TOOL_STRIPES) { + border.bottom = 1; + } + } + myTabs.getComponent().setBorder(new EmptyBorder(1, 0, 0, 0)); - final List rightIds = ((ToolWindowManagerEx)ToolWindowManager.getInstance(myProject)).getIdsOn(ToolWindowAnchor.RIGHT); - myTabs.getPresentation().setPaintBorder(-1, -1, rightIds.size() > 0 ? 1 : 0, -1).setTabSidePaintBorder(5); + + myTabs.getPresentation().setPaintBorder(border.top, border.left, border.right, border.bottom).setTabSidePaintBorder(5); } public Component getComponent() { diff --git a/platform/platform-impl/src/com/intellij/openapi/keymap/impl/KeymapImpl.java b/platform/platform-impl/src/com/intellij/openapi/keymap/impl/KeymapImpl.java index e94bf582ad5d..9fd40c39fac4 100644 --- a/platform/platform-impl/src/com/intellij/openapi/keymap/impl/KeymapImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/keymap/impl/KeymapImpl.java @@ -134,7 +134,10 @@ public class KeymapImpl implements Keymap, ExternalizableScheme { public KeymapImpl deriveKeymap() { - if (!canModify()) { + if (canModify()) { + return copy(false); + } + else { KeymapImpl newKeymap = new KeymapImpl(); newKeymap.myParent = this; @@ -142,9 +145,6 @@ public class KeymapImpl implements Keymap, ExternalizableScheme { newKeymap.myCanModify = canModify(); return newKeymap; } - else { - return copy(false); - } } public KeymapImpl copy(boolean copyExternalInfo) { diff --git a/platform/platform-impl/src/com/intellij/openapi/options/SchemesManagerFactoryImpl.java b/platform/platform-impl/src/com/intellij/openapi/options/SchemesManagerFactoryImpl.java index 7ac5c6f594a1..08ad51313907 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/SchemesManagerFactoryImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/SchemesManagerFactoryImpl.java @@ -21,15 +21,13 @@ import com.intellij.openapi.application.impl.ApplicationImpl; import com.intellij.openapi.components.RoamingType; import com.intellij.openapi.components.SettingsSavingComponent; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.util.WriteExternalException; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import org.jetbrains.annotations.NotNull; - public class SchemesManagerFactoryImpl extends SchemesManagerFactory implements SettingsSavingComponent { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.options.SchemesManagerFactoryImpl"); @@ -62,7 +60,7 @@ public class SchemesManagerFactoryImpl extends SchemesManagerFactory implements return Collections.emptyList(); } - public void exportScheme(final E scheme, final String name, final String description) throws WriteExternalException { + public void exportScheme(final E scheme, final String name, final String description) { } public boolean isImportAvailable() { @@ -73,7 +71,7 @@ public class SchemesManagerFactoryImpl extends SchemesManagerFactory implements return false; } - public void save() throws WriteExternalException { + public void save() { } protected void onSchemeDeleted(final Scheme toDelete) { diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/ColorProgressBar.java b/platform/platform-impl/src/com/intellij/openapi/progress/util/ColorProgressBar.java index 31743acc7005..410d7be15fbe 100644 --- a/platform/platform-impl/src/com/intellij/openapi/progress/util/ColorProgressBar.java +++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/ColorProgressBar.java @@ -149,7 +149,7 @@ public class ColorProgressBar extends JComponent { g.setClip(4, 3, size.width - 8, size.height - 6); - int bricksToDraw = getBricksToDraw(myFraction); + int bricksToDraw = myFraction == 0 ? 0 : getBricksToDraw(myFraction); if (myIndeterminate) { diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/InternalDecorator.java b/platform/platform-impl/src/com/intellij/openapi/wm/impl/InternalDecorator.java index 200c4aef79ff..4f34130ebde3 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/InternalDecorator.java +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/InternalDecorator.java @@ -17,6 +17,7 @@ package com.intellij.openapi.wm.impl; import com.intellij.ide.DataManager; import com.intellij.ide.actions.ResizeToolWindowAction; +import com.intellij.ide.ui.UISettings; import com.intellij.idea.ActionsBundle; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; @@ -32,10 +33,7 @@ import com.intellij.openapi.ui.Queryable; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.wm.ToolWindowAnchor; -import com.intellij.openapi.wm.ToolWindowContentUiType; -import com.intellij.openapi.wm.ToolWindowManager; -import com.intellij.openapi.wm.ToolWindowType; +import com.intellij.openapi.wm.*; import com.intellij.openapi.wm.ex.ToolWindowEx; import com.intellij.ui.InplaceButton; import com.intellij.ui.UIBundle; @@ -427,7 +425,7 @@ public final class InternalDecorator extends JPanel implements Queryable, TypeSa innerPanel.add(toolWindowComponent, BorderLayout.CENTER); final NonOpaquePanel inner = new NonOpaquePanel(innerPanel); - inner.setBorder(new InnerPanelBorder()); + inner.setBorder(new InnerPanelBorder(myToolWindow)); contentPane.add(inner, BorderLayout.CENTER); add(contentPane, BorderLayout.CENTER); @@ -441,15 +439,30 @@ public final class InternalDecorator extends JPanel implements Queryable, TypeSa } private static class InnerPanelBorder implements Border { + + private ToolWindow myWindow; + + private InnerPanelBorder(ToolWindow window) { + myWindow = window; + } + public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) { g.setColor(UIUtil.getBorderInactiveColor()); UIUtil.drawLine(g, x, y, x, y + height - 2); UIUtil.drawLine(g, x + width - 1, y, x + width - 1, y + height - 2); - UIUtil.drawLine(g, x + 1, y + height - 1, x + width - 2, y + height - 1); + + if (hasBottomLine()) { + UIUtil.drawLine(g, x + 1, y + height - 1, x + width - 2, y + height - 1); + } + } + + private boolean hasBottomLine() { + return (myWindow.getAnchor() == ToolWindowAnchor.BOTTOM || myWindow.getAnchor() == ToolWindowAnchor.LEFT || myWindow.getAnchor() == ToolWindowAnchor.RIGHT) + && !UISettings.getInstance().HIDE_TOOL_STRIPES && UISettings.getInstance().SHOW_STATUS_BAR; } public Insets getBorderInsets(final Component c) { - return new Insets(0, 1, 1, 1); + return new Insets(0, 1, hasBottomLine() ? 1 : 0, 1); } public boolean isBorderOpaque() { diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.java b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.java index 218fafec124e..2f1e4b03e7e6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/ToolWindowManagerImpl.java @@ -49,6 +49,7 @@ import com.intellij.ui.awt.RelativePoint; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.CollectionFactory; import com.intellij.util.containers.HashMap; +import com.intellij.util.ui.PositionTracker; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.UiNotifyConnector; import org.jdom.Element; @@ -1050,7 +1051,7 @@ public final class ToolWindowManagerImpl extends ToolWindowManagerEx implements Icon actualIcon = icon != null ? icon : type.getDefaultIcon(); final Balloon balloon = - JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text.replace("\n", "
"), actualIcon, type.getPopupBackground(), listener) + JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(text.replace("\n", "
"), actualIcon, type.getPopupBackground(), listener).setHideOnClickOutside(false).setHideOnFrameResize(false) .createBalloon(); Disposer.register(balloon, new Disposable() { public void dispose() { @@ -1060,6 +1061,7 @@ public final class ToolWindowManagerImpl extends ToolWindowManagerEx implements stripe.repaint(); } }); + Disposer.register(getProject(), balloon); final StripeButton button = stripe.getButtonFor(toolWindowId); if (button == null) return; @@ -1067,8 +1069,24 @@ public final class ToolWindowManagerImpl extends ToolWindowManagerEx implements final Runnable show = new Runnable() { public void run() { if (button.isShowing()) { - final Point point = new Point(button.getBounds().width / 2, button.getHeight() / 2 - 2); - balloon.show(new RelativePoint(button, point), position.get()); + PositionTracker tracker = new PositionTracker(button) { + @Override + public RelativePoint recalculateLocation(Balloon object) { + Stripe twStripe = myToolWindowsPane.getStripeFor(toolWindowId); + StripeButton twButton = twStripe != null ? twStripe.getButtonFor(toolWindowId) : null; + + if (twButton == null) return null; + + if (getToolWindow(toolWindowId).getAnchor() != anchor) { + object.hide(); + return null; + } + + final Point point = new Point(twButton.getBounds().width / 2, twButton.getHeight() / 2 - 2); + return new RelativePoint(twButton, point); + } + }; + balloon.show(tracker, position.get()); } else { final Rectangle bounds = myToolWindowsPane.getBounds(); diff --git a/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java b/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java index 5a1cd609db40..c3c4621b0c06 100644 --- a/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java @@ -29,10 +29,7 @@ import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.panels.NonOpaquePanel; import com.intellij.ui.components.panels.Wrapper; import com.intellij.util.Alarm; -import com.intellij.util.ui.Animator; -import com.intellij.util.ui.BaseButtonBehavior; -import com.intellij.util.ui.TimedDeadzone; -import com.intellij.util.ui.UIUtil; +import com.intellij.util.ui.*; import org.jetbrains.annotations.Nullable; import javax.swing.*; @@ -45,7 +42,7 @@ import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.util.concurrent.CopyOnWriteArraySet; -public class BalloonImpl implements Disposable, Balloon, LightweightWindow { +public class BalloonImpl implements Disposable, Balloon, LightweightWindow, PositionTracker.Client { private MyComponent myComp; private JLayeredPane myLayeredPane; @@ -96,6 +93,11 @@ public class BalloonImpl implements Disposable, Balloon, LightweightWindow { } } + if (event instanceof MouseEvent && UIUtil.isCloseClick((MouseEvent)event)) { + hide(); + return; + } + if (myHideOnKey && (event.getID() == KeyEvent.KEY_PRESSED)) { final KeyEvent ke = (KeyEvent)event; if (SwingUtilities.isDescendingFrom(ke.getComponent(), myComp) || ke.getComponent() == myComp) return; @@ -110,6 +112,7 @@ public class BalloonImpl implements Disposable, Balloon, LightweightWindow { private final CopyOnWriteArraySet myListeners = new CopyOnWriteArraySet(); private boolean myVisible; + private PositionTracker myTracker; private boolean isInsideBalloon(MouseEvent me) { if (!me.getComponent().isShowing()) return true; @@ -188,13 +191,41 @@ public class BalloonImpl implements Disposable, Balloon, LightweightWindow { show(target, pos); } + public void show(PositionTracker tracker, Balloon.Position position) { + Position pos = BELOW; + switch (position) { + case atLeft: + pos = AT_LEFT; + break; + case atRight: + pos = AT_RIGHT; + break; + case below: + pos = BELOW; + break; + case above: + pos = ABOVE; + break; + } + + show(tracker, pos); + } + + private void show(RelativePoint target, Position position) { + show(new PositionTracker.Static(target), position); + } + + private void show(PositionTracker tracker, Position position) { if (isVisible()) return; assert !myDisposed : "Balloon is already disposed"; - assert target.getComponent().isShowing() : "Target component is not showing: " + target; + assert tracker.getComponent().isShowing() : "Target component is not showing: " + tracker; - final Window window = SwingUtilities.getWindowAncestor(target.getComponent()); + myTracker = tracker; + myTracker.init(this); + + final Window window = SwingUtilities.getWindowAncestor(tracker.getComponent()); JRootPane root = null; if (window instanceof JFrame) { @@ -219,7 +250,7 @@ public class BalloonImpl implements Disposable, Balloon, LightweightWindow { : new EmptyBorder(getNormalInset(), getNormalInset(), getNormalInset(), getNormalInset()); myComp = new MyComponent(myContent, this, border); - myTargetPoint = target.getPoint(myLayeredPane); + myTargetPoint = tracker.recalculateLocation(this).getPoint(myLayeredPane); myComp.clear(); myComp.myAlpha = 0f; @@ -247,6 +278,15 @@ public class BalloonImpl implements Disposable, Balloon, LightweightWindow { KeyEvent.KEY_EVENT_MASK); } + public void revalidate(PositionTracker tracker) { + RelativePoint newPosition = tracker.recalculateLocation(this); + + if (newPosition != null) { + myTargetPoint = newPosition.getPoint(myLayeredPane); + myPosition.updateLocation(this); + } + } + public void show(JLayeredPane pane) { show(pane, null); } diff --git a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java index bf3a130fb19f..f478155c40ca 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java @@ -118,6 +118,8 @@ public class AbstractPopup implements JBPopup { private Runnable myFinalRunnable; + protected boolean myOk; + protected final SpeedSearch mySpeedSearch = new SpeedSearch() { boolean searchFieldShown = false; protected void update() { @@ -460,6 +462,11 @@ public class AbstractPopup implements JBPopup { return relativePoint; } + public final void closeOk(@Nullable InputEvent e) { + setOk(true); + cancel(e); + } + public final void cancel() { cancel(null); } @@ -505,7 +512,7 @@ public class AbstractPopup implements JBPopup { if (myListeners != null) { for (JBPopupListener each : myListeners) { - each.onClosed(new LightweightWindowEvent(this)); + each.onClosed(new LightweightWindowEvent(this, myOk)); } } } @@ -1253,4 +1260,8 @@ public class AbstractPopup implements JBPopup { public void setFinalRunnable(Runnable finalRunnable) { myFinalRunnable = finalRunnable; } + + public void setOk(boolean ok) { + myOk = ok; + } } diff --git a/platform/platform-impl/src/com/intellij/ui/popup/WizardPopup.java b/platform/platform-impl/src/com/intellij/ui/popup/WizardPopup.java index 6d5d990c30f3..fbb5bdecdc3d 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/WizardPopup.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/WizardPopup.java @@ -420,4 +420,13 @@ public abstract class WizardPopup extends AbstractPopup implements ActionListene getParent().setFinalRunnable(runnable); } } + + @Override + public void setOk(boolean ok) { + if (getParent() == null) { + super.setOk(ok); + } else { + getParent().setOk(ok); + } + } } diff --git a/platform/platform-impl/src/com/intellij/ui/popup/list/ListPopupImpl.java b/platform/platform-impl/src/com/intellij/ui/popup/list/ListPopupImpl.java index 3f7847aad0fc..ba05ea8c7d66 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/list/ListPopupImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/list/ListPopupImpl.java @@ -302,6 +302,7 @@ public class ListPopupImpl extends WizardPopup implements ListPopup { if (myListModel.getSize() == 0) { setFinalRunnable(myStep.getFinalRunnable()); + setOk(true); disposeAllParents(e); setIndexForShowingChild(-1); return true; @@ -337,6 +338,7 @@ public class ListPopupImpl extends WizardPopup implements ListPopup { return false; } else { + setOk(true); setFinalRunnable(myStep.getFinalRunnable()); disposeAllParents(e); setIndexForShowingChild(-1); diff --git a/platform/platform-impl/src/com/intellij/ui/popup/tree/TreePopupImpl.java b/platform/platform-impl/src/com/intellij/ui/popup/tree/TreePopupImpl.java index 06d214216f78..7127fac42880 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/tree/TreePopupImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/tree/TreePopupImpl.java @@ -324,6 +324,7 @@ public class TreePopupImpl extends WizardPopup implements TreePopup { final PopupStep queriedStep = myStep.onChosen(userObject, handleFinalChoices); if (queriedStep == PopupStep.FINAL_CHOICE || !hasNextStep) { setFinalRunnable(myStep.getFinalRunnable()); + setOk(true); disposeAllParents(e); } else { diff --git a/platform/platform-resources-en/src/messages/CommonBundle.properties b/platform/platform-resources-en/src/messages/CommonBundle.properties index ddadf78cb1be..c4b7df98c6fb 100644 --- a/platform/platform-resources-en/src/messages/CommonBundle.properties +++ b/platform/platform-resources-en/src/messages/CommonBundle.properties @@ -1,6 +1,7 @@ button.yes=&Yes button.no=&No dialog.options.do.not.show=Do not show this dialog in the future +dialog.options.do.not.ask=Remember, don't ask again dialog.title.choose.date=Choose Date dialog.ioexception.proxy=Set up HTTP proxy settings dialog.ioexception.tryagain=Try again diff --git a/platform/testFramework/src/com/intellij/testFramework/PlatformTestCase.java b/platform/testFramework/src/com/intellij/testFramework/PlatformTestCase.java index d64219b951eb..6139fac76449 100644 --- a/platform/testFramework/src/com/intellij/testFramework/PlatformTestCase.java +++ b/platform/testFramework/src/com/intellij/testFramework/PlatformTestCase.java @@ -90,7 +90,6 @@ public abstract class PlatformTestCase extends UsefulTestCase implements DataPro private static TestCase ourTestCase = null; public static final long DEFAULT_TEST_TIME = 300L; public static long ourTestTime = DEFAULT_TEST_TIME; - private static final MyThreadGroup MY_THREAD_GROUP = new MyThreadGroup(); private static final String ourOriginalTempDir = FileUtil.getTempDirectory(); private EditorListenerTracker myEditorListenerTracker; private String myTempDirPath; @@ -302,11 +301,6 @@ public abstract class PlatformTestCase extends UsefulTestCase implements DataPro setTmpDir(ourOriginalTempDir); - Throwable fromThreadGroup = MY_THREAD_GROUP.popThrowable(); - if (fromThreadGroup != null) { - throw new RuntimeException(fromThreadGroup); - } - if (!myAssertionsInTestDetected) { if (IdeaLogger.ourErrorsOccurred != null) { throw IdeaLogger.ourErrorsOccurred; @@ -408,35 +402,21 @@ public abstract class PlatformTestCase extends UsefulTestCase implements DataPro } public void runBare() throws Throwable { - final Throwable[] throwable = new Throwable[1]; - - Thread thread = new Thread(MY_THREAD_GROUP, new Runnable() { - public void run() { - try { - runBareImpl(); - } - catch (Throwable th) { - throwable[0] = th; - } finally { - try { - ApplicationManager.getApplication().invokeAndWait(new Runnable() { - public void run() { - cleanupApplicationCaches(getProject()); - resetAllFields(); - } - }, ModalityState.NON_MODAL); + try { + runBareImpl(); + } + finally { + try { + ApplicationManager.getApplication().invokeAndWait(new Runnable() { + public void run() { + cleanupApplicationCaches(getProject()); + resetAllFields(); } - catch (Throwable e) { - // Ignore - } - } + }, ModalityState.NON_MODAL); + } + catch (Throwable e) { + // Ignore } - }, "IDEA Test Case Thread"); - thread.start(); - thread.join(); - - if (throwable[0] != null) { - throw throwable[0]; } } @@ -583,13 +563,13 @@ public abstract class PlatformTestCase extends UsefulTestCase implements DataPro private static void setTmpDir(String path) { System.setProperty("java.io.tmpdir", path); + FileUtil.resetCanonicalTempPathCache(); + try { Class ioFile = File.class; Field field = ioFile.getDeclaredField("tmpdir"); - field.setAccessible(true); field.set(ioFile, null); - FileUtil.resetCanonicalTempPathCache(); } catch (NoSuchFieldException ignore) { // field was removed in JDK 1.6.0_12 @@ -599,29 +579,6 @@ public abstract class PlatformTestCase extends UsefulTestCase implements DataPro } } - private static class MyThreadGroup extends ThreadGroup { - private Throwable myThrowable; - @NonNls private static final String IDEATEST_THREAD_GROUP = "IDEATest"; - - private MyThreadGroup() { - super(IDEATEST_THREAD_GROUP); - } - - public void uncaughtException(Thread t, Throwable e) { - myThrowable = e; - super.uncaughtException(t, e); - } - - public Throwable popThrowable() { - try { - return myThrowable; - } - finally { - myThrowable = null; - } - } - } - public static void initPlatformLangPrefix() { initPlatformPrefix("com.intellij.openapi.project.impl.IdeaProjectManagerImpl", "PlatformLangXml"); } diff --git a/platform/testFramework/src/com/intellij/testFramework/fixtures/TempDirTestFixture.java b/platform/testFramework/src/com/intellij/testFramework/fixtures/TempDirTestFixture.java index 267f5a376d3c..dd2ffcc03a7a 100644 --- a/platform/testFramework/src/com/intellij/testFramework/fixtures/TempDirTestFixture.java +++ b/platform/testFramework/src/com/intellij/testFramework/fixtures/TempDirTestFixture.java @@ -17,6 +17,7 @@ package com.intellij.testFramework.fixtures; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileFilter; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -31,6 +32,8 @@ public interface TempDirTestFixture extends IdeaTestFixture { VirtualFile copyAll(String dataDir, String targetDir); + VirtualFile copyAll(String dataDir, String targetDir, @NotNull VirtualFileFilter filter); + String getTempDirPath(); VirtualFile getFile(@NonNls String path); diff --git a/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/LightTempDirTestFixtureImpl.java b/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/LightTempDirTestFixtureImpl.java index 818e59c3a2c3..3ef392658bc0 100644 --- a/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/LightTempDirTestFixtureImpl.java +++ b/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/LightTempDirTestFixtureImpl.java @@ -18,10 +18,7 @@ package com.intellij.testFramework.fixtures.impl; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VfsUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.*; import com.intellij.testFramework.LightPlatformTestCase; import com.intellij.testFramework.fixtures.TempDirTestFixture; import com.intellij.util.PathUtil; @@ -90,7 +87,11 @@ public class LightTempDirTestFixtureImpl extends BaseFixture implements TempDirT }); } - public VirtualFile copyAll(final String dataDir, final String targetDir) { + public VirtualFile copyAll(String dataDir, String targetDir) { + return copyAll(dataDir, targetDir, VirtualFileFilter.ALL); + } + + public VirtualFile copyAll(final String dataDir, final String targetDir, @NotNull final VirtualFileFilter filter) { return ApplicationManager.getApplication().runWriteAction(new Computable() { public VirtualFile compute() { final VirtualFile from = LocalFileSystem.getInstance().refreshAndFindFileByPath(dataDir); @@ -101,7 +102,7 @@ public class LightTempDirTestFixtureImpl extends BaseFixture implements TempDirT tempDir = findOrCreateChildDir(tempDir, targetDir); } - VfsUtil.copyDirectory(this, from, tempDir, null); + VfsUtil.copyDirectory(this, from, tempDir, filter); return tempDir; } catch (IOException e) { diff --git a/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/TempDirTestFixtureImpl.java b/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/TempDirTestFixtureImpl.java index ba5b8672ec84..1827d2ca2bc0 100644 --- a/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/TempDirTestFixtureImpl.java +++ b/platform/testFramework/src/com/intellij/testFramework/fixtures/impl/TempDirTestFixtureImpl.java @@ -23,6 +23,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileFilter; import com.intellij.testFramework.fixtures.TempDirTestFixture; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -50,7 +51,11 @@ public class TempDirTestFixtureImpl extends BaseFixture implements TempDirTestFi } } - public VirtualFile copyAll(final String dataDir, final String targetDir) { + public VirtualFile copyAll(String dataDir, String targetDir) { + return copyAll(dataDir, targetDir, VirtualFileFilter.ALL); + } + + public VirtualFile copyAll(final String dataDir, final String targetDir, @NotNull final VirtualFileFilter filter) { createTempDirectory(); return ApplicationManager.getApplication().runWriteAction(new Computable() { public VirtualFile compute() { @@ -67,7 +72,7 @@ public class TempDirTestFixtureImpl extends BaseFixture implements TempDirTestFi } final VirtualFile from = LocalFileSystem.getInstance().refreshAndFindFileByPath(dataDir); assert from != null : dataDir + " not found"; - VfsUtil.copyDirectory(null, from, tempDir, null); + VfsUtil.copyDirectory(null, from, tempDir, filter); return tempDir; } catch (IOException e) { diff --git a/platform/util/src/com/intellij/util/containers/hash/HashUtil.java b/platform/util/src/com/intellij/util/containers/hash/HashUtil.java index 33af77bdc61b..7b42f713a856 100644 --- a/platform/util/src/com/intellij/util/containers/hash/HashUtil.java +++ b/platform/util/src/com/intellij/util/containers/hash/HashUtil.java @@ -16,15 +16,12 @@ package com.intellij.util.containers.hash; - import java.util.Arrays; - class HashUtil { - public static final int MIN_CAPACITY = 5; public static final float DEFAULT_LOAD_FACTOR = 1; @@ -32,15 +29,13 @@ class HashUtil { public static final float CAPACITY_MULTIPLE = 1.618033989f; - public static int hash(Object key) { - return key.hashCode() & 0x7fffffff; + return key == null ? 0 : key.hashCode() & 0x7fffffff; } - public static int adjustTableSize(int size) { int i = Arrays.binarySearch(tableSizes, size); @@ -56,101 +51,95 @@ class HashUtil { } - private static final int[] tableSizes = { + // include all odd primes under 1000 - // include all odd primes under 1000 + 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, - 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, + 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, - 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, + 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, - 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, + 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, - 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, + 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, - 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, + 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, - 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, + 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, - 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, + 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, - 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, + 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, - 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, + 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, - 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, + 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, - 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, - - 947, 953, 967, 971, 977, 983, 991, 997, + 947, 953, 967, 971, 977, 983, 991, 997, + 1031, 2063, 4127, 8263, 16529, 33071, 66161, 132329, 264659, 529327, 1058657, 2117317, 4234651, 8469319, - 1031, 2063, 4127, 8263, 16529, 33071, 66161, 132329, 264659, 529327, 1058657, 2117317, 4234651, 8469319, + 1091, 2203, 4409, 8819, 17657, 35317, 70639, 141283, 282571, 565163, 1130351, 2260717, 4521509, 9043033, - 1091, 2203, 4409, 8819, 17657, 35317, 70639, 141283, 282571, 565163, 1130351, 2260717, 4521509, 9043033, + 1153, 2309, 4621, 9257, 18517, 37039, 74093, 148193, 296437, 592877, 1185787, 2371609, 4743223, 9486469, - 1153, 2309, 4621, 9257, 18517, 37039, 74093, 148193, 296437, 592877, 1185787, 2371609, 4743223, 9486469, + 1217, 2437, 4877, 9767, 19541, 39089, 78179, 156361, 312727, 625477, 1250969, 2501953, 5003909, 10007819, - 1217, 2437, 4877, 9767, 19541, 39089, 78179, 156361, 312727, 625477, 1250969, 2501953, 5003909, 10007819, + 1283, 2579, 5167, 10337, 20681, 41381, 82763, 165527, 331063, 662141, 1324313, 2648629, 5297287, 10594583, - 1283, 2579, 5167, 10337, 20681, 41381, 82763, 165527, 331063, 662141, 1324313, 2648629, 5297287, 10594583, + 1361, 2729, 5471, 10949, 21911, 43853, 87719, 175447, 350899, 701819, 1403641, 2807303, 5614657, 11229331, - 1361, 2729, 5471, 10949, 21911, 43853, 87719, 175447, 350899, 701819, 1403641, 2807303, 5614657, 11229331, + 1409, 2819, 5639, 11279, 22567, 45137, 90281, 180563, 361159, 722321, 1444649, 2889317, 5778649, 11557303, - 1409, 2819, 5639, 11279, 22567, 45137, 90281, 180563, 361159, 722321, 1444649, 2889317, 5778649, 11557303, + 1481, 2963, 5927, 11863, 23741, 47491, 94993, 189989, 379979, 759959, 1519939, 3039917, 6079861, 12159731, - 1481, 2963, 5927, 11863, 23741, 47491, 94993, 189989, 379979, 759959, 1519939, 3039917, 6079861, 12159731, + 1543, 3089, 6197, 12401, 24809, 49627, 99257, 198529, 397063, 794137, 1588289, 3176597, 6353213, 12706451, - 1543, 3089, 6197, 12401, 24809, 49627, 99257, 198529, 397063, 794137, 1588289, 3176597, 6353213, 12706451, + 1601, 3203, 6421, 12853, 25717, 51437, 102877, 205759, 411527, 823117, 1646237, 3292489, 6584983, 13169977, - 1601, 3203, 6421, 12853, 25717, 51437, 102877, 205759, 411527, 823117, 1646237, 3292489, 6584983, 13169977, + 1667, 3343, 6689, 13381, 26777, 53569, 107171, 214351, 428731, 857471, 1714957, 3429961, 6859933, 13719869, - 1667, 3343, 6689, 13381, 26777, 53569, 107171, 214351, 428731, 857471, 1714957, 3429961, 6859933, 13719869, + 1733, 3467, 6947, 13901, 27803, 55609, 111227, 222461, 444929, 889871, 1779761, 3559537, 7119103, 14238221, - 1733, 3467, 6947, 13901, 27803, 55609, 111227, 222461, 444929, 889871, 1779761, 3559537, 7119103, 14238221, + 1801, 3607, 7219, 14447, 28901, 57803, 115613, 231241, 462491, 924997, 1850021, 3700043, 7400123, 14800271, - 1801, 3607, 7219, 14447, 28901, 57803, 115613, 231241, 462491, 924997, 1850021, 3700043, 7400123, 14800271, + 1861, 3727, 7457, 14923, 29851, 59707, 119417, 238837, 477677, 955363, 1910729, 3821483, 7643017, 15286079, - 1861, 3727, 7457, 14923, 29851, 59707, 119417, 238837, 477677, 955363, 1910729, 3821483, 7643017, 15286079, + 1931, 3863, 7727, 15461, 30931, 61871, 123757, 247519, 495041, 990137, 1980281, 3960581, 7921169, - 1931, 3863, 7727, 15461, 30931, 61871, 123757, 247519, 495041, 990137, 1980281, 3960581, 7921169, - - 1993, 3989, 7993, 15991, 31991, 63997, 127997, 256019, 512047, 1024099, 2048203, 4096427, 8192867, + 1993, 3989, 7993, 15991, 31991, 63997, 127997, 256019, 512047, 1024099, 2048203, 4096427, 8192867, + 26339969, 52679969, 105359939, 210719881, 421439783, 842879579, 1685759167, - 26339969, 52679969, 105359939, 210719881, 421439783, 842879579, 1685759167, + 28977863, 57955739, 115911563, 231823147, 463646329, 927292699, 1854585413, - 28977863, 57955739, 115911563, 231823147, 463646329, 927292699, 1854585413, + 31322867, 62645741, 125291483, 250582987, 501165979, 1002331963, 2004663929, - 31322867, 62645741, 125291483, 250582987, 501165979, 1002331963, 2004663929, + 17135863, 34271747, 68543509, 137087021, 274174111, 548348231, 1096696463, - 17135863, 34271747, 68543509, 137087021, 274174111, 548348231, 1096696463, + 18366923, 36733847, 73467739, 146935499, 293871013, 587742049, 1175484103, - 18366923, 36733847, 73467739, 146935499, 293871013, 587742049, 1175484103, + 19845871, 39691759, 79383533, 158767069, 317534141, 635068283, 1270136683, - 19845871, 39691759, 79383533, 158767069, 317534141, 635068283, 1270136683, + 21006137, 42012281, 84024581, 168049163, 336098327, 672196673, 1344393353, - 21006137, 42012281, 84024581, 168049163, 336098327, 672196673, 1344393353, + 22458671, 44917381, 89834777, 179669557, 359339171, 718678369, 1437356741, - 22458671, 44917381, 89834777, 179669557, 359339171, 718678369, 1437356741, + 23723597, 47447201, 94894427, 189788857, 379577741, 759155483, 1518310967, - 23723597, 47447201, 94894427, 189788857, 379577741, 759155483, 1518310967, - - 25002389, 50004791, 100009607, 200019221, 400038451, 800076929, 1600153859, + 25002389, 50004791, 100009607, 200019221, 400038451, 800076929, 1600153859, - - Integer.MAX_VALUE, + Integer.MAX_VALUE, }; - static { Arrays.sort(tableSizes); diff --git a/platform/util/src/com/intellij/util/containers/hash/LinkedHashMap.java b/platform/util/src/com/intellij/util/containers/hash/LinkedHashMap.java index 2183531b487f..d8be8d642fe7 100644 --- a/platform/util/src/com/intellij/util/containers/hash/LinkedHashMap.java +++ b/platform/util/src/com/intellij/util/containers/hash/LinkedHashMap.java @@ -82,7 +82,7 @@ public class LinkedHashMap extends AbstractMap implements Map return e.setValue(value); } } - final Entry e = new Entry(key, value); + final Entry e = new Entry(key, value, hash); e.hashNext = table[index]; table[index] = e; final Entry top = this.top; @@ -230,9 +230,9 @@ public class LinkedHashMap extends AbstractMap implements Map private Entry previous; private Entry hashNext; - public Entry(final K key, final V value) { + public Entry(final K key, final V value, int hash) { this.key = key; - keyHash = HashUtil.hash(key); + keyHash = hash; this.value = value; } @@ -360,4 +360,4 @@ public class LinkedHashMap extends AbstractMap implements Map LinkedHashMap.this.clear(); } } -} \ No newline at end of file +} diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsVFSListener.java b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsVFSListener.java index 1e1d3aadfab5..c8272d12d5c5 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/VcsVFSListener.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/VcsVFSListener.java @@ -27,6 +27,7 @@ import com.intellij.openapi.vcs.actions.VcsContextFactory; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.*; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; +import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -235,8 +236,21 @@ public abstract class VcsVFSListener implements Disposable { } public void beforeFileDeletion(final VirtualFileEvent event) { - if (!isEventIgnored(event)) { - addFileToDelete(event.getFile()); + final VirtualFile file = event.getFile(); + if (isEventIgnored(event)) { return; } + if (!myChangeListManager.isIgnoredFile(file)) { + addFileToDelete(file); + return; + } + // files are ignored, directories are handled recursively + if (event.getFile().isDirectory()) { + final List list = new LinkedList(); + VcsUtil.collectFiles(file, list, true, isDirectoryVersioningSupported()); + for (VirtualFile child : list) { + if (!myChangeListManager.isIgnoredFile(child)) { + addFileToDelete(child); + } + } } } diff --git a/platform/vcs-api/src/com/intellij/util/ui/VcsBackgroundTask.java b/platform/vcs-api/src/com/intellij/util/ui/VcsBackgroundTask.java new file mode 100644 index 000000000000..3ee9cb91bd26 --- /dev/null +++ b/platform/vcs-api/src/com/intellij/util/ui/VcsBackgroundTask.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2007 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.util.ui; + +import com.intellij.openapi.progress.PerformInBackgroundOption; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.AbstractVcsHelper; +import com.intellij.openapi.vcs.VcsException; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author yole + */ +public abstract class VcsBackgroundTask extends Task.ConditionalModal { + private final Collection myItems; + private final List myExceptions = new ArrayList(); + + public VcsBackgroundTask(final Project project, @NotNull final String title, @NotNull final PerformInBackgroundOption backgroundOption, + final Collection itemsToProcess) { + super(project, title, false, backgroundOption); + myItems = itemsToProcess; + } + + public void run(@NotNull ProgressIndicator indicator) { + for(T item: myItems) { + try { + process(item); + } + catch(VcsException ex) { + myExceptions.add(ex); + } + } + } + + public void onSuccess() { + if (!myExceptions.isEmpty()) { + AbstractVcsHelper.getInstance(myProject).showErrors(myExceptions, myTitle); + } + } + + protected abstract void process(T item) throws VcsException; +} diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/GroovyBundle.properties b/plugins/groovy/src/org/jetbrains/plugins/groovy/GroovyBundle.properties index b7706dbea799..9278c82be7a4 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/GroovyBundle.properties +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/GroovyBundle.properties @@ -337,3 +337,4 @@ wildcards.are.not.allowed.in.extends.list=A super type may not specify a wildcar method.doesnot.override.super=Method does not override method from its super class method.duplicate=Method with signature {0} is already defined in the class ''{1}'' ambiguous.code.block=Ambiguous code block +cyclic.inheritance.involving.0=Cyclic inheritance involving ''{0}'' diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java index a5b1eb8eb46e..c307956b4a33 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java @@ -39,6 +39,7 @@ import com.intellij.psi.util.MethodSignatureBackedByPsiMethod; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.IncorrectOperationException; +import com.intellij.util.containers.HashSet; import com.intellij.util.containers.MultiMap; import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; @@ -1033,6 +1034,41 @@ public class GroovyAnnotator extends GroovyElementVisitor implements Annotator { checkForWildCards(holder, implementsClause); checkDuplicateClass(typeDefinition, holder); + + checkCyclicInheritance(holder, typeDefinition); + } + + private static void checkCyclicInheritance(AnnotationHolder holder, + GrTypeDefinition typeDefinition) { + final PsiClass psiClass = getCircularClass(typeDefinition, new HashSet()); + if (psiClass != null) { + holder.createErrorAnnotation(typeDefinition.getNameIdentifierGroovy(), + GroovyBundle.message("cyclic.inheritance.involving.0", psiClass.getQualifiedName())); + } + } + + private static PsiClass getCircularClass(PsiClass aClass, Collection usedClasses) { + if (usedClasses.contains(aClass)) { + return aClass; + } + try { + usedClasses.add(aClass); + PsiClass[] superTypes = aClass.getSupers(); + for (PsiElement superType : superTypes) { + while (superType instanceof PsiClass) { + if (!"java.lang.Object".equals(((PsiClass)superType).getQualifiedName())) { + PsiClass circularClass = getCircularClass((PsiClass)superType, usedClasses); + if (circularClass != null) return circularClass; + } + // check class qualifier + superType = superType.getParent(); + } + } + } + finally { + usedClasses.remove(aClass); + } + return null; } private static void checkForWildCards(AnnotationHolder holder, @Nullable GrReferenceList clause) { diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyPropertyUtils.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyPropertyUtils.java index e112eceff2dc..34b6645480c6 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyPropertyUtils.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyPropertyUtils.java @@ -148,7 +148,7 @@ public class GroovyPropertyUtils { if (methodName.startsWith(GET_PREFIX) && methodName.length() > 3) { return decapitalize(methodName.substring(3)); } - else if (methodName.startsWith(IS_PREFIX) && methodName.length() > 2 && canBeBoolean) { + if (canBeBoolean && methodName.startsWith(IS_PREFIX) && methodName.length() > 2) { return decapitalize(methodName.substring(2)); } return null; diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyHighlightingTest.java b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyHighlightingTest.java index a476f8863986..958dfd00073c 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyHighlightingTest.java +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/GroovyHighlightingTest.java @@ -247,4 +247,6 @@ public class GroovyHighlightingTest extends LightCodeInsightFixtureTestCase { public void testAmbiguousCodeBlock() throws Exception {doTest();} public void testNotAmbiguousClosableBlock() throws Exception {doTest();} public void testDuplicateParameterInClosableBlock() throws Exception {doTest();} + + public void testCyclicInheritance() throws Exception {doTest();} } \ No newline at end of file diff --git a/plugins/groovy/testdata/highlighting/CircularInheritance.groovy b/plugins/groovy/testdata/highlighting/CircularInheritance.groovy index 22eb749d3965..ebbc607cec60 100644 --- a/plugins/groovy/testdata/highlighting/CircularInheritance.groovy +++ b/plugins/groovy/testdata/highlighting/CircularInheritance.groovy @@ -1,4 +1,4 @@ -class Foo extends Bar {} -class Bar extends Foo {} +class Foo extends Bar {} +class Bar extends Foo {} println(new Foo()) \ No newline at end of file diff --git a/plugins/groovy/testdata/highlighting/CyclicInheritance.groovy b/plugins/groovy/testdata/highlighting/CyclicInheritance.groovy new file mode 100644 index 000000000000..79936c6ea806 --- /dev/null +++ b/plugins/groovy/testdata/highlighting/CyclicInheritance.groovy @@ -0,0 +1,15 @@ +class A extends C { + +} + +class B extends A { + +} + +class C extends B { + +} + +class D extends B { + +} \ No newline at end of file diff --git a/plugins/hg4idea/hg4idea.iml b/plugins/hg4idea/hg4idea.iml index b8677676d493..ccb452415959 100644 --- a/plugins/hg4idea/hg4idea.iml +++ b/plugins/hg4idea/hg4idea.iml @@ -13,6 +13,7 @@ + diff --git a/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties b/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties index acd0c6cce8f3..baa7501fcff7 100644 --- a/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties +++ b/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties @@ -55,11 +55,21 @@ hg4idea.commit.error.unknown=Could not commit, no error message was provided hg4idea.commit.repository.title=Commit Repositories hg4idea.commit.repository.body=Commit all changes in all affected repositories -hg4idea.add.confirmation.title=Add file(s) -hg4idea.add.confirmation.body=Do you want to schedule the following file for addition to Mercurial?\n{0} +hg4idea.add.title=Add files to Mercurial +hg4idea.add.single.title=Add file to Mercurial +hg4idea.add.body=Do you want to add the following file to Mercurial?\n{0}\n\nIf you say NO, you can still add it later manually. +hg4idea.add.progress=Adding files to Mercurial -hg4idea.delete.confirmation.title=Delete file(s) -hg4idea.delete.confirmation.body=Do you want to schedule the following file for deletion from Mercurial?\n{0} +hg4idea.remove.single.title=Remove file from Mercurial +hg4idea.remove.single.body=Do you want to remove the following file from Mercurial?\n{0}\n\nIf you say NO, you can still remove it later manually. +hg4idea.remove.commit.single.body=Do you want to remove the following file from Mercurial?\n{0}\n\nIf you say NO, the file will be excluded from the commit, but you can still remove it later manually. +hg4idea.remove.multiple.title=Remove files from Mercurial +hg4idea.remove.commit.multiple.title=Select files to remove from Mercurial +hg4idea.remove.commit.multiple.description=The files you select will be scheduled on deletion from Mercurial and then committed.
Others won't be neither removed from the repository, nor committed. +hg4idea.remove.button.ok=Remove +hg4idea.remove.progress=Removing files from the VCS... + +hg4idea.move.progress=Moving files in the VCS... hg4idea.update.error.uncommittedMerge=outstanding uncommitted merge in repository {0}, not updating or merging hg4idea.update.error.localchanges=outstanding uncommitted changes in repository {0}, not merging with pulled head @@ -87,4 +97,4 @@ hg4idea.status.currentSituation.description=Current mercurial branch and p hg4idea.warning.no-default-update-path=Skipped \"{0}\". No default update path. hg4idea.merge.please-commit=Merged heads, please commit repository \"{0}\" hg4idea.error.invalidExecutable=\"{0}\" is not a valid mercurial executable -hg4idea.integrate.other.head=Other head: {0} +hg4idea.integrate.other.head=Other head: {0} \ No newline at end of file diff --git a/plugins/hg4idea/src/META-INF/plugin.xml b/plugins/hg4idea/src/META-INF/plugin.xml index 0406e1850618..98ab049147e2 100644 --- a/plugins/hg4idea/src/META-INF/plugin.xml +++ b/plugins/hg4idea/src/META-INF/plugin.xml @@ -27,6 +27,7 @@ + diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgFile.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgFile.java index d55998c3aed9..c1303b834d13 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgFile.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgFile.java @@ -13,6 +13,7 @@ package org.zmlx.hg4idea; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vfs.VfsUtil; @@ -42,6 +43,10 @@ public class HgFile { this(vcsRoot, filePath.getIOFile()); } + public HgFile(@NotNull Project project, @NotNull VirtualFile file) { + this(VcsUtil.getVcsRootFor(project, file), VcsUtil.getFilePath(file.getPath())); + } + @NotNull public VirtualFile getRepo() { return vcsRoot; diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java new file mode 100644 index 000000000000..ac0f6e8320eb --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandler.java @@ -0,0 +1,130 @@ +package org.zmlx.hg4idea; + +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.TitlePanel; +import com.intellij.openapi.vcs.*; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.CommitExecutor; +import com.intellij.openapi.vcs.changes.ui.*; +import com.intellij.openapi.vcs.checkin.CheckinHandler; +import com.intellij.util.PairConsumer; +import com.intellij.util.ui.ConfirmationDialog; +import gnu.trove.THashSet; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeModel; +import java.awt.*; +import java.util.*; +import java.util.List; + +/** + * HgRemoveCheckinHandler scans the changes which are ready for commit + * for files, which were deleted on the file system, but not from the VCS, + * and proposes a dialog to select files which are to be removed from the VCS. + */ +public class HgRemoveCheckinHandler extends CheckinHandler { + + private final CheckinProjectPanel myCheckinPanel; + private final Project myProject; + + public HgRemoveCheckinHandler(CheckinProjectPanel checkinPanel) { + myCheckinPanel = checkinPanel; + myProject = checkinPanel.getProject(); + } + + @Override + public ReturnResult beforeCheckin(CommitExecutor executor, final PairConsumer additionalDataConsumer) { + // find missing changes + final List missingChanges = new LinkedList(); + for (Change c : myCheckinPanel.getSelectedChanges()) { + if (c.getFileStatus() == FileStatus.DELETED_FROM_FS) { + missingChanges.add(c); + } + } + + if (missingChanges.isEmpty()) { + return ReturnResult.COMMIT; + } + + // show a simple confirmation for 1 missing change, or more complex dialog for 2 or more changes + final Collection changesToRemove = new THashSet(); + VcsShowConfirmationOption confirmation = ProjectLevelVcsManager.getInstance(myProject).getStandardConfirmation(VcsConfiguration.StandardConfirmation.REMOVE, HgVcs.getInstance(myProject)); + if (missingChanges.size() == 1) { + if (ConfirmationDialog + .requestForConfirmation(confirmation, myProject, + HgVcsMessages.message("hg4idea.remove.commit.single.body", missingChanges.get(0).getBeforeRevision().getFile().getPresentableUrl()), + HgVcsMessages.message("hg4idea.remove.single.title"), Messages.getQuestionIcon())) { + changesToRemove.add(missingChanges.get(0)); + } + } else { + final SelectMissingChangesDialog dialog = new SelectMissingChangesDialog(myProject, missingChanges, confirmation); + dialog.show(); + if (dialog.isOK()) { + changesToRemove.addAll(dialog.getSelectedChanges()); + } else { + return ReturnResult.CANCEL; + } + } + + if (!changesToRemove.isEmpty()) { + removeChangesAndUpdateCommitted(changesToRemove, additionalDataConsumer); + } + return ReturnResult.COMMIT; + } + + private void removeChangesAndUpdateCommitted(final Collection changesToRemove, + final PairConsumer additionalDataConsumer) { + ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { + public void run() { + final List filepathsToRemove = new ArrayList(changesToRemove.size()); + for (Change c : changesToRemove) { + filepathsToRemove.add(c.getBeforeRevision().getFile()); + } + HgUtil.removeFilesFromVcs(myProject, filepathsToRemove); + additionalDataConsumer.consume(HgVcs.getInstance(myProject), changesToRemove); + } + }, HgVcsMessages.message("hg4idea.remove.progress"), true, myProject); + } + + private class SelectMissingChangesDialog extends AbstractSelectFilesDialog { + + public SelectMissingChangesDialog(final Project project, List originalFiles, VcsShowConfirmationOption confirmation) { + super(project, false, confirmation, null); + myFileList = new ChangesTreeList(project, originalFiles, true, true, null, null) { + protected DefaultTreeModel buildTreeModel(final List changes, ChangeNodeDecorator changeNodeDecorator) { + return new TreeModelBuilder(project, false).buildModel(changes, changeNodeDecorator); + } + + protected List getSelectedObjects(final ChangesBrowserNode node) { + return node.getAllChangesUnder(); + } + + protected Change getLeadSelectedObject(final ChangesBrowserNode node) { + final Object o = node.getUserObject(); + if (o instanceof Change) { + return (Change) o; + } + return null; + } + }; + myFileList.setChangesToDisplay(originalFiles); + myPanel.add(myFileList, BorderLayout.CENTER); + setOKButtonText(HgVcsMessages.message("hg4idea.remove.button.ok")); + setTitle(HgVcsMessages.message("hg4idea.remove.multiple.title")); + init(); + } + + public Collection getSelectedChanges() { + return myFileList.getIncludedChanges(); + } + + protected JComponent createTitlePane() { + return new TitlePanel(HgVcsMessages.message("hg4idea.remove.commit.multiple.title"), HgVcsMessages.message( + "hg4idea.remove.commit.multiple.description")); + } + + } + +} \ No newline at end of file diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java new file mode 100644 index 000000000000..78f0ec9e2e20 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgRemoveCheckinHandlerFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zmlx.hg4idea; + +import com.intellij.openapi.vcs.CheckinProjectPanel; +import com.intellij.openapi.vcs.checkin.CheckinHandler; +import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory; +import org.jetbrains.annotations.NotNull; + +/** + * HgRemoveCheckinHandlerFactory provides the {@link CheckinHandler} which scans + * the changes list for files, which were deleted on the file system, but not from + * the VCS. + * + * @author Kirill Likhodedov + */ +public class HgRemoveCheckinHandlerFactory extends CheckinHandlerFactory { + + @NotNull + @Override + public CheckinHandler createHandler(final CheckinProjectPanel checkinPanel) { + return new HgRemoveCheckinHandler(checkinPanel); + } + +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgUtil.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgUtil.java index ad88c5d986b7..52b77a2a0d7e 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgUtil.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgUtil.java @@ -12,16 +12,19 @@ // limitations under the License. package org.zmlx.hg4idea; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; +import com.intellij.openapi.application.*; +import com.intellij.openapi.project.*; +import com.intellij.openapi.util.ShutDownTracker; import com.intellij.openapi.vcs.FilePath; -import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.vcsUtil.VcsUtil; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.*; +import com.intellij.openapi.vfs.*; +import com.intellij.vcsUtil.*; import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.command.HgRemoveCommand; import java.io.*; +import java.util.List; /** * TODO JavaDoc. @@ -134,8 +137,7 @@ public abstract class HgUtil { try { final File file = copyResourceToTempFile(base, ".py"); final String fileName = file.getName(); - Runtime.getRuntime().addShutdownHook(new Thread(){ - @Override + ShutDownTracker.getInstance().registerShutdownTask(new Runnable() { public void run() { File[] files = file.getParentFile().listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { @@ -154,4 +156,22 @@ public abstract class HgUtil { return null; } } + + /** + * Calls 'hg remove' to remove given files from the VCS. + * @param project + * @param files files to be removed from the VCS. + */ + public static void removeFilesFromVcs(Project project, List files) { + final HgRemoveCommand command = new HgRemoveCommand(project); + for (FilePath filePath : files) { + final VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath); + if (vcsRoot == null) { + continue; + } + command.execute(new HgFile(vcsRoot, filePath)); + } + } + + } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgVFSListener.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgVFSListener.java new file mode 100644 index 000000000000..cd54d7e5884f --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgVFSListener.java @@ -0,0 +1,139 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zmlx.hg4idea; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.VcsConfiguration; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.VcsVFSListener; +import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.VcsBackgroundTask; +import com.intellij.vcsUtil.VcsUtil; +import org.zmlx.hg4idea.command.HgAddCommand; +import org.zmlx.hg4idea.command.HgCopyCommand; +import org.zmlx.hg4idea.command.HgMoveCommand; +import org.zmlx.hg4idea.command.HgRemoveCommand; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Listens to VFS events (such as adding or deleting bunch of files) and performs necessary operations with the VCS. + * @author Kirill Likhodedov + */ +public class HgVFSListener extends VcsVFSListener { + + private final VcsDirtyScopeManager dirtyScopeManager; + + protected HgVFSListener(final Project project, final HgVcs vcs) { + super(project, vcs); + dirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject); + } + + @Override + protected String getAddTitle() { + return HgVcsMessages.message("hg4idea.add.title"); + } + + @Override + protected String getSingleFileAddTitle() { + return HgVcsMessages.message("hg4idea.add.single.title"); + } + + @Override + protected String getSingleFileAddPromptTemplate() { + return HgVcsMessages.message("hg4idea.add.body"); + } + + @Override + protected void performAdding(Collection addedFiles, final Map copyFromMap) { + (new VcsBackgroundTask(myProject, + HgVcsMessages.message("hg4idea.add.progress"), + VcsConfiguration.getInstance(myProject).getAddRemoveOption(), + addedFiles) { + protected void process(final VirtualFile file) throws VcsException { + if (file.isDirectory()) { + return; + } + final VirtualFile copyFrom = copyFromMap.get(file); + if (copyFrom != null) { + (new HgCopyCommand(myProject)).execute(new HgFile(myProject, copyFrom), new HgFile(myProject, file)); + } else { + (new HgAddCommand(myProject)).execute(new HgFile(myProject, file)); + } + dirtyScopeManager.fileDirty(file); + } + + }).queue(); + } + + @Override + protected String getDeleteTitle() { + return HgVcsMessages.message("hg4idea.remove.multiple.title"); + } + + @Override + protected String getSingleFileDeleteTitle() { + return HgVcsMessages.message("hg4idea.remove.single.title"); + } + + @Override + protected String getSingleFileDeletePromptTemplate() { + return HgVcsMessages.message("hg4idea.remove.single.body"); + } + + @Override + protected void performDeletion(List filesToDelete) { + (new VcsBackgroundTask(myProject, + HgVcsMessages.message("hg4idea.remove.progress"), + VcsConfiguration.getInstance(myProject).getAddRemoveOption(), + filesToDelete) { + protected void process(final FilePath file) throws VcsException { + if (file.isDirectory()) { + return; + } + (new HgRemoveCommand(myProject)).execute(new HgFile(VcsUtil.getVcsRootFor(myProject, file), file)); + dirtyScopeManager.fileDirty(file); + } + + }).queue(); + } + + @Override + protected void performMoveRename(List movedFiles) { + (new VcsBackgroundTask(myProject, + HgVcsMessages.message("hg4idea.move.progress"), + VcsConfiguration.getInstance(myProject).getAddRemoveOption(), + movedFiles) { + protected void process(final MovedFileInfo file) throws VcsException { + final FilePath source = VcsUtil.getFilePath(file.myOldPath); + final FilePath target = VcsUtil.getFilePath(file.myNewPath); + (new HgMoveCommand(myProject)).execute(new HgFile(VcsUtil.getVcsRootFor(myProject, source), source), new HgFile(VcsUtil.getVcsRootFor(myProject, target), target)); + dirtyScopeManager.fileDirty(source); + dirtyScopeManager.fileDirty(target); + } + + }).queue(); + } + + @Override + protected boolean isDirectoryVersioningSupported() { + return false; + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgVcs.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgVcs.java index f31fa0bd6e6c..b245312417b0 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgVcs.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgVcs.java @@ -12,45 +12,32 @@ // limitations under the License. package org.zmlx.hg4idea; -import com.intellij.concurrency.JobScheduler; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.fileEditor.FileEditorManagerAdapter; -import com.intellij.openapi.fileEditor.FileEditorManagerEvent; -import com.intellij.openapi.fileEditor.FileEditorManagerListener; -import com.intellij.openapi.options.Configurable; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.IconLoader; -import com.intellij.openapi.vcs.AbstractVcs; -import com.intellij.openapi.vcs.CommittedChangesProvider; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.annotate.AnnotationProvider; -import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vcs.changes.ChangeProvider; -import com.intellij.openapi.vcs.checkin.CheckinEnvironment; -import com.intellij.openapi.vcs.diff.DiffProvider; -import com.intellij.openapi.vcs.history.VcsHistoryProvider; -import com.intellij.openapi.vcs.rollback.RollbackEnvironment; -import com.intellij.openapi.vcs.update.UpdateEnvironment; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.StatusBar; -import com.intellij.openapi.wm.WindowManager; -import com.intellij.util.messages.MessageBus; -import com.intellij.util.messages.MessageBusConnection; -import com.intellij.util.messages.Topic; +import com.intellij.concurrency.*; +import com.intellij.openapi.application.*; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.options.*; +import com.intellij.openapi.project.*; +import com.intellij.openapi.util.*; +import com.intellij.openapi.vcs.*; +import com.intellij.openapi.vcs.annotate.*; +import com.intellij.openapi.vcs.changes.*; +import com.intellij.openapi.vcs.checkin.*; +import com.intellij.openapi.vcs.diff.*; +import com.intellij.openapi.vcs.history.*; +import com.intellij.openapi.vcs.rollback.*; +import com.intellij.openapi.vcs.update.*; +import com.intellij.openapi.vfs.*; +import com.intellij.openapi.wm.*; +import com.intellij.util.messages.*; import org.zmlx.hg4idea.provider.*; -import org.zmlx.hg4idea.provider.annotate.HgAnnotationProvider; -import org.zmlx.hg4idea.provider.commit.HgCheckinEnvironment; -import org.zmlx.hg4idea.provider.commit.HgCommitExecutor; -import org.zmlx.hg4idea.provider.update.HgIntegrateEnvironment; -import org.zmlx.hg4idea.provider.update.HgUpdateEnvironment; -import org.zmlx.hg4idea.ui.HgChangesetStatus; -import org.zmlx.hg4idea.ui.HgCurrentBranchStatus; +import org.zmlx.hg4idea.provider.annotate.*; +import org.zmlx.hg4idea.provider.commit.*; +import org.zmlx.hg4idea.provider.update.*; +import org.zmlx.hg4idea.ui.*; import javax.swing.*; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.io.File; +import java.util.concurrent.*; public class HgVcs extends AbstractVcs { @@ -72,7 +59,6 @@ public class HgVcs extends AbstractVcs { private final HgChangeProvider changeProvider; private final HgProjectConfigurable configurable; - private final HgVirtualFileListener virtualFileListener; private final HgRollbackEnvironment rollbackEnvironment; private final HgDiffProvider diffProvider; private final HgHistoryProvider historyProvider; @@ -91,6 +77,7 @@ public class HgVcs extends AbstractVcs { private final HgProjectSettings projectSettings; private boolean started = false; + private HgVFSListener myVFSListener; public HgVcs(Project project, HgGlobalSettings globalSettings, HgProjectSettings projectSettings) { @@ -99,7 +86,6 @@ public class HgVcs extends AbstractVcs { this.projectSettings = projectSettings; configurable = new HgProjectConfigurable(projectSettings); changeProvider = new HgChangeProvider(project, getKeyInstanceMethod()); - virtualFileListener = new HgVirtualFileListener(project, this); rollbackEnvironment = new HgRollbackEnvironment(project); diffProvider = new HgDiffProvider(project); historyProvider = new HgHistoryProvider(project); @@ -243,7 +229,6 @@ public class HgVcs extends AbstractVcs { return; } - LocalFileSystem.getInstance().addVirtualFileListener(virtualFileListener); ChangeListManager.getInstance(myProject).registerCommitExecutor(commitExecutor); StatusBar statusBar = WindowManager.getInstance().getStatusBar(myProject); @@ -289,6 +274,8 @@ public class HgVcs extends AbstractVcs { } } ); + + myVFSListener = new HgVFSListener(myProject, this); } @Override @@ -297,7 +284,6 @@ public class HgVcs extends AbstractVcs { return; } - LocalFileSystem.getInstance().removeVirtualFileListener(virtualFileListener); StatusBar statusBar = WindowManager.getInstance().getStatusBar(myProject); if (messageBusConnection != null) { messageBusConnection.disconnect(); @@ -310,10 +296,36 @@ public class HgVcs extends AbstractVcs { //statusBar.removeCustomIndicationComponent(outgoingChangesStatus); //statusBar.removeCustomIndicationComponent(hgCurrentBranchStatus); } + + if (myVFSListener != null) { + Disposer.dispose(myVFSListener); + myVFSListener = null; + } } public static HgVcs getInstance(Project project) { return (HgVcs) ProjectLevelVcsManager.getInstance(project).findVcsByName(VCS_NAME); } + private static String ourTestHgExecutablePath; // path to hg in test mode + + /** + * Sets the path to hg executable used in the test mode. + */ + public static void setTestHgExecutablePath(String path) { + ourTestHgExecutablePath = path; + } + + /** + * Returns the hg executable file. + * If it is a test, returns the special value set in the test setup. + * If it is a normal app, returns the value from global settings. + */ + public String getHgExecutable() { + if (ApplicationManager.getApplication().isUnitTestMode()) { + return (new File(ourTestHgExecutablePath, SystemInfo.isWindows ? "hg.exe" : "hg")).getPath(); + } + return globalSettings.getHgExecutable(); + } + } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgVirtualFileListener.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgVirtualFileListener.java deleted file mode 100644 index dde9e9da2413..000000000000 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgVirtualFileListener.java +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright 2008-2010 Victor Iacoban -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -// either express or implied. See the License for the specific language governing permissions and -// limitations under the License. -package org.zmlx.hg4idea; - -import com.intellij.openapi.fileTypes.FileTypeManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vcs.*; -import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vfs.*; -import com.intellij.vcsUtil.VcsUtil; -import org.jetbrains.annotations.NotNull; -import org.zmlx.hg4idea.command.*; - -import java.io.File; -import java.util.Arrays; - -import static com.intellij.openapi.vcs.VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY; -import static com.intellij.openapi.vcs.VcsShowConfirmationOption.Value.SHOW_CONFIRMATION; - -public class HgVirtualFileListener extends VirtualFileAdapter { - - private final Project project; - private final AbstractVcs vcs; - - public HgVirtualFileListener(Project project, AbstractVcs vcs) { - this.project = project; - this.vcs = vcs; - } - - @Override - public void fileCopied(VirtualFileCopyEvent event) { - if (event.isFromRefresh()) { - return; - } - final VirtualFile newFile = event.getFile(); - FilePath newPath = getFilePath(newFile); - VirtualFile newRepo = VcsUtil.getVcsRootFor(project, newFile); - boolean newFileProcessable = newRepo != null && VcsUtil.isFileForVcs(newFile, project, vcs) && isFileProcessable(newFile); - - final VirtualFile oldFile = event.getOriginalFile(); - FilePath oldPath = getFilePath(oldFile); - VirtualFile oldRepo = VcsUtil.getVcsRootFor(project, oldFile); - boolean oldFileProcessable = oldRepo != null && VcsUtil.isFileForVcs(oldFile, project, vcs) && isFileProcessable(oldFile); - - if (newFileProcessable && oldFileProcessable && oldRepo.equals(newRepo)) { - copyFile(newRepo, oldPath, newPath); - markDirty(newPath); - } else if (newFileProcessable) { - addFile(newRepo, newPath, false); - markDirty(newPath); - } - } - - @Override - public void fileMoved(VirtualFileMoveEvent event) { - VirtualFile oldParent = event.getOldParent(); - String fileName = event.getFileName(); - - FilePath oldPath = VcsUtil.getFilePath(new File(new File(oldParent.getPath()), fileName)); - VirtualFile oldRepo = VcsUtil.getVcsRootFor(project, oldPath); - boolean oldFileProcessable = oldRepo != null && VcsUtil.isFileForVcs(oldPath, project, vcs) && isFileProcessable(oldPath); - - VirtualFile newFile = event.getFile(); - FilePath newPath = getFilePath(newFile); - VirtualFile newRepo = VcsUtil.getVcsRootFor(project, newFile); - boolean newFileProcessable = newRepo != null && VcsUtil.isFileForVcs(newFile, project, vcs) && isFileProcessable(newFile); - - if (newFileProcessable && oldFileProcessable && oldRepo.equals(newRepo)) { - moveFile(oldRepo, newRepo, oldPath, newPath); - } else { - HgFileStatusEnum oldStatus = getStatus(oldRepo, oldPath); - boolean silent = oldStatus != HgFileStatusEnum.UNVERSIONED; - - if (oldFileProcessable) { - deleteFile(oldRepo, oldPath, silent); - } - - if (newFileProcessable) { - addFile(newRepo, newPath, silent); - } - } - - markDirty(oldPath); - markDirty(newPath); - } - - @Override - public void fileCreated(VirtualFileEvent event) { - if (event.isFromRefresh()) { - return; - } - final VirtualFile file = event.getFile(); - if (!VcsUtil.isFileForVcs(file, project, vcs)) { - return; - } - if (!isFileProcessable(file) || file.isDirectory()) { - return; - } - - VirtualFile repo = VcsUtil.getVcsRootFor(project, file); - if (repo == null) { - return; - } - - FilePath path = getFilePath(file); - addFile(repo, path, false); - markDirty(path); - } - - @Override - public void fileDeleted(VirtualFileEvent event) { - if (event.isFromRefresh()) { - return; - } - - final VirtualFile file = event.getFile(); - - if (!shouldProcess(file)) { - return; - } - - VirtualFile repo = VcsUtil.getVcsRootFor(project, file); - if (repo == null) { - return; - } - - FilePath path = getFilePath(file); - deleteFile(repo, path, false); - markDirty(path); - } - - @Override - public void propertyChanged(VirtualFilePropertyEvent event) { - if (VirtualFile.PROP_NAME.equals(event.getPropertyName())) { - fileRenamed(event); - } - } - - private void fileRenamed(VirtualFilePropertyEvent event) { - String oldName = (String) event.getOldValue(); - VirtualFile oldParent = event.getParent(); - - FilePath oldPath = VcsUtil.getFilePath(new File(new File(oldParent.getPath()), oldName)); - VirtualFile oldRepo = VcsUtil.getVcsRootFor(project, oldPath); - boolean oldFileProcessable = oldRepo != null && VcsUtil.isFileForVcs(oldPath, project, vcs) && isFileProcessable(oldPath); - - VirtualFile newFile = event.getFile(); - FilePath newPath = getFilePath(newFile); - VirtualFile newRepo = VcsUtil.getVcsRootFor(project, newFile); - boolean newFileProcessable = newRepo != null && VcsUtil.isFileForVcs(newFile, project, vcs) && isFileProcessable(newFile); - - if (newFileProcessable && oldFileProcessable && oldRepo.equals(newRepo)) { - renameFile(newRepo, oldPath, newPath); - } else { - if (oldFileProcessable) { - deleteFile(oldRepo, oldPath, false); - } - - if (newFileProcessable) { - addFile(newRepo, newPath, false); - } - } - markDirty(oldPath); - markDirty(newPath); - } - - private void markDirty(final FilePath path) { - HgUtil.markDirectoryDirty(project, path.getParentPath()); - } - - private FilePath getFilePath(VirtualFile file) { - return VcsUtil.getFilePath(file.getPath()); - } - - private void addFile(@NotNull VirtualFile repo, @NotNull FilePath path, boolean silent) { - if (silent || checkAdd(path)) { - new HgAddCommand(project).execute(new HgFile(repo, path)); - } - } - - private void copyFile(VirtualFile repo, FilePath oldPath, FilePath newPath) { - if (checkAdd(newPath)) { - HgCopyCommand command = new HgCopyCommand(project); - HgFile source = new HgFile(repo, oldPath); - HgFile target = new HgFile(repo, newPath); - command.execute(source, target); - } - } - - private void moveFile(@NotNull VirtualFile repo, VirtualFile newRepo, @NotNull FilePath oldPath, @NotNull FilePath newPath) { - HgMoveCommand command = new HgMoveCommand(project); - HgFile source = new HgFile(repo, oldPath); - HgFile target = new HgFile(repo, newPath); - command.execute(source, target); - } - - private void renameFile(@NotNull VirtualFile repo, @NotNull FilePath oldPath, @NotNull FilePath newPath) { - HgMoveCommand command = new HgMoveCommand(project); - HgFile source = new HgFile(repo, oldPath); - HgFile target = new HgFile(repo, newPath); - command.execute(source, target); - } - - private boolean checkAdd(FilePath path) { - String title = HgVcsMessages.message("hg4idea.add.confirmation.title"); - String message = HgVcsMessages.message("hg4idea.add.confirmation.body", path.getPath()); - - VcsShowConfirmationOption option = ProjectLevelVcsManager.getInstance(project) - .getStandardConfirmation(VcsConfiguration.StandardConfirmation.ADD, vcs); - - boolean processAdd = false; - if (DO_ACTION_SILENTLY == option.getValue()) { - processAdd = true; - } else if (SHOW_CONFIRMATION == option.getValue()) { - AbstractVcsHelper helper = AbstractVcsHelper.getInstance(project); - processAdd = null != helper.selectFilePathsToProcess( - Arrays.asList(path), title, null, title, message, option - ); - } - return processAdd; - } - - private void deleteFile(@NotNull VirtualFile repo, @NotNull FilePath path, boolean silent) { - HgFileStatusEnum status = getStatus(repo, path); - if (status == HgFileStatusEnum.UNVERSIONED || status == HgFileStatusEnum.IGNORED) { - return; - } - - String title = HgVcsMessages.message("hg4idea.delete.confirmation.title"); - String message = HgVcsMessages.message("hg4idea.delete.confirmation.body", path.getPath()); - - boolean processDelete = false; - - VcsShowConfirmationOption option = ProjectLevelVcsManager.getInstance(project) - .getStandardConfirmation(VcsConfiguration.StandardConfirmation.REMOVE, vcs); - - if (DO_ACTION_SILENTLY == option.getValue() || status == HgFileStatusEnum.ADDED || silent) { - processDelete = true; - } else if (SHOW_CONFIRMATION == option.getValue()) { - AbstractVcsHelper helper = AbstractVcsHelper.getInstance(project); - processDelete = null != helper.selectFilePathsToProcess( - Arrays.asList(path), title, null, title, message, option - ); - } - - if (processDelete) { - new HgRemoveCommand(project).execute(new HgFile(repo, path)); - } - } - - private HgFileStatusEnum getStatus(VirtualFile repo, FilePath file) { - HgStatusCommand status = new HgStatusCommand(project); - HgChange change = status.execute(repo, file.getPath()); - return change != null ? change.getStatus() : HgFileStatusEnum.UNVERSIONED; - } - - private boolean shouldProcess(VirtualFile file) { - return VcsUtil.isFileForVcs(file, project, vcs) && isFileProcessable(file); - } - - private boolean isFileProcessable(VirtualFile file) { - if (file == null) { - return false; - } - ChangeListManager changeListManager = ChangeListManager.getInstance(project); - return !FileTypeManager.getInstance().isFileIgnored(file.getName()) - || !changeListManager.isIgnoredFile(file); - } - - private boolean isFileProcessable(FilePath file) { - if (file == null) { - return false; - } - return !FileTypeManager.getInstance().isFileIgnored(file.getName()); - } -} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgCommandService.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgCommandService.java index 60ec054459a5..ddc5381d88bb 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgCommandService.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgCommandService.java @@ -20,6 +20,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.vcsUtil.VcsUtil; import org.zmlx.hg4idea.HgGlobalSettings; import org.zmlx.hg4idea.HgUtil; +import org.zmlx.hg4idea.HgVcs; import org.zmlx.hg4idea.HgVcsMessages; import javax.swing.*; @@ -73,7 +74,7 @@ public final class HgCommandService { HgCommandResult execute(VirtualFile repo, List hgOptions, String operation, List arguments, Charset charset) { List cmdLine = new LinkedList(); - cmdLine.add(settings.getHgExecutable()); + cmdLine.add(HgVcs.getInstance(project).getHgExecutable()); if (repo != null) { cmdLine.add("--repository"); cmdLine.add(repo.getPath()); @@ -129,7 +130,7 @@ public final class HgCommandService { private void showError(Exception e) { StringBuilder message = new StringBuilder(); message.append(HgVcsMessages.message("hg4idea.command.executable.error", - settings.getHgExecutable())) + HgVcs.getInstance(project).getHgExecutable())) .append("\n") .append("Original Error:\n") .append(e.getMessage()); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgResolveCommand.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgResolveCommand.java index 15946754397f..71f873f4a228 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgResolveCommand.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgResolveCommand.java @@ -30,7 +30,7 @@ import static org.zmlx.hg4idea.HgErrorHandler.ensureSuccess; public class HgResolveCommand { - private static final File FILEMERGE_PLUGIN = HgUtil.getTemporaryPythonFile("filemerge"); + private static File FILEMERGE_PLUGIN; private static final int ITEM_COUNT = 3; @@ -38,6 +38,9 @@ public class HgResolveCommand { public HgResolveCommand(Project project) { this.project = project; + if (FILEMERGE_PLUGIN == null) { + FILEMERGE_PLUGIN = HgUtil.getTemporaryPythonFile("filemerge"); + } } public Map list(VirtualFile repo) { diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/commit/HgCheckinEnvironment.java b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/commit/HgCheckinEnvironment.java index f1e0c624394d..698868ba2cb7 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/commit/HgCheckinEnvironment.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/commit/HgCheckinEnvironment.java @@ -18,6 +18,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vcs.CheckinProjectPanel; import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeList; @@ -33,16 +34,20 @@ import org.jetbrains.annotations.NotNull; import org.zmlx.hg4idea.HgFile; import org.zmlx.hg4idea.HgRevisionNumber; import org.zmlx.hg4idea.HgVcsMessages; +import gnu.trove.THashSet; +import org.zmlx.hg4idea.*; + import org.zmlx.hg4idea.command.*; import java.util.*; +import java.util.List; public class HgCheckinEnvironment implements CheckinEnvironment { - private final Project project; + private final Project myProject; public HgCheckinEnvironment(Project project) { - this.project = project; + this.myProject = project; } public RefreshableOnComponent createAdditionalOptionsPanel(CheckinProjectPanel panel, @@ -64,25 +69,36 @@ public class HgCheckinEnvironment implements CheckinEnvironment { @SuppressWarnings({"ThrowableInstanceNeverThrown"}) public List commit(List changes, String preparedComment, @NotNull NullableFunction parametersHolder) { - List exceptions = new LinkedList(); - for (Map.Entry> entry : getFilesByRepository(changes).entrySet()) { + final List exceptions = new LinkedList(); + final Collection removedChanges = (Collection) parametersHolder.fun(HgVcs.getInstance(myProject)); - VirtualFile repo = entry.getKey(); - Set selectedFiles = entry.getValue(); + for (final Map.Entry> entry : groupChangesByRepository(changes).entrySet()) { + // separate commit for each repository + final VirtualFile repo = entry.getKey(); + final HgCommitCommand command = new HgCommitCommand(myProject, repo, preparedComment); + + // commit files, except those which were deleted from filesystem, but not from the VCS. + // HgRemoveCheckinHandler proposes to remove such files from the VCS before commit. + // If some of those weren't removed, it was done intentionally, so just silently ignore them. + final Set selectedFiles = new THashSet(); + for (Change c : entry.getValue()) { + if (c.getFileStatus() == FileStatus.DELETED_FROM_FS) { + if (removedChanges == null || !removedChanges.contains(c)) { // missing and not removed from vcs via the HgRemoveCheckinHandler + continue; + } + } + final FilePath filepath = (c.getAfterRevision() == null ? c.getBeforeRevision().getFile() : c.getAfterRevision().getFile()); + selectedFiles.add(new HgFile(repo, filepath)); + } - HgCommitCommand command = new HgCommitCommand(project, repo, preparedComment); - if (isMergeCommit(repo)) { //partial commits are not allowed during merges //verifyResult that all changed files in the repo are selected //If so, commit the entire repository //If not, abort - Set changedFilesNotInCommit = getChangedFilesNotInCommit(repo, selectedFiles); - boolean partial = !changedFilesNotInCommit.isEmpty(); - - - if (partial) { + final Set changedFilesNotInCommit = getChangedFilesNotInCommit(repo, selectedFiles); + if (!changedFilesNotInCommit.isEmpty()) { final StringBuilder filesNotIncludedString = new StringBuilder(); for (HgFile hgFile : changedFilesNotInCommit) { filesNotIncludedString.append("
  • "); @@ -93,12 +109,17 @@ public class HgCheckinEnvironment implements CheckinEnvironment { //abort return exceptions; } + // else : all was included, or it was OK to commit everything, + // so no need to set the files on the command, because then mercurial will complain + } + } + else { + if (selectedFiles.isEmpty()) { // nothing to commit. Aborting here, because otherwise 'hg commit' without specifying files will commit all files. + return exceptions; } - // else : all was included, or it was OK to commit everything, - // so no need to set the files on the command, because then mercurial will complain - } else { command.setFiles(selectedFiles); } + try { command.execute(); } catch (HgCommandException e) { @@ -111,13 +132,13 @@ public class HgCheckinEnvironment implements CheckinEnvironment { } private boolean isMergeCommit(VirtualFile repo) { - return new HgWorkingCopyRevisionsCommand(project).parents(repo).size() > 1; + return new HgWorkingCopyRevisionsCommand(myProject).parents(repo).size() > 1; } private Set getChangedFilesNotInCommit(VirtualFile repo, Set selectedFiles) { - List parents = new HgWorkingCopyRevisionsCommand(project).parents(repo); + List parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo); - HgStatusCommand statusCommand = new HgStatusCommand(project); + HgStatusCommand statusCommand = new HgStatusCommand(myProject); statusCommand.setBaseRevision(parents.get(0)); statusCommand.setIncludeUnknown(false); statusCommand.setIncludeIgnored(false); @@ -142,7 +163,7 @@ public class HgCheckinEnvironment implements CheckinEnvironment { Runnable runnable = new Runnable() { public void run() { choice[0] = Messages.showOkCancelDialog( - project, + myProject, HgVcsMessages.message("hg4idea.commit.partial.merge.message", filesNotIncludedString), HgVcsMessages.message("hg4idea.commit.partial.merge.title"), null @@ -163,21 +184,14 @@ public class HgCheckinEnvironment implements CheckinEnvironment { } public List scheduleMissingFileForDeletion(List files) { - HgRemoveCommand command = new HgRemoveCommand(project); - for (FilePath filePath : files) { - VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath); - if (vcsRoot == null) { - continue; - } - command.execute(new HgFile(vcsRoot, filePath)); - } + HgUtil.removeFilesFromVcs(myProject, files); return null; } public List scheduleUnversionedFilesForAddition(List files) { - HgAddCommand command = new HgAddCommand(project); + HgAddCommand command = new HgAddCommand(myProject); for (VirtualFile file : files) { - VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, file); + VirtualFile vcsRoot = VcsUtil.getVcsRootFor(myProject, file); if (vcsRoot == null) { continue; } @@ -190,39 +204,32 @@ public class HgCheckinEnvironment implements CheckinEnvironment { return false; } - private Map> getFilesByRepository(List changes) { - Map> result = new HashMap>(); + /** + * Groups the changes by repository roots. + * @param changes the list of all changes. + * @return Changes grouped by repository roots. + */ + private Map> groupChangesByRepository(List changes) { + final Map> result = new HashMap>(); for (Change change : changes) { - ContentRevision afterRevision = change.getAfterRevision(); - ContentRevision beforeRevision = change.getBeforeRevision(); + final ContentRevision afterRevision = change.getAfterRevision(); + final ContentRevision beforeRevision = change.getBeforeRevision(); + assert beforeRevision != null || afterRevision != null; // nothing-to-nothing change cannot happen. + final FilePath filePath = (afterRevision != null) ? afterRevision.getFile() : beforeRevision.getFile(); - if (afterRevision != null) { - addFile(result, afterRevision.getFile()); + final VirtualFile repo = VcsUtil.getVcsRootFor(myProject, filePath); + if (repo == null || filePath.isDirectory()) { + continue; } - if (beforeRevision != null) { - addFile(result, beforeRevision.getFile()); + + List repoChanges = result.get(repo); + if (repoChanges == null) { + repoChanges = new ArrayList(); + result.put(repo, repoChanges); } + repoChanges.add(change); } return result; } - private void addFile(Map> result, FilePath filePath) { - if (filePath == null) { - return; - } - - VirtualFile repo = VcsUtil.getVcsRootFor(project, filePath); - if (repo == null || filePath.isDirectory()) { - return; - } - - Set hgFiles = result.get(repo); - if (hgFiles == null) { - hgFiles = new HashSet(); - result.put(repo, hgFiles); - } - - hgFiles.add(new HgFile(repo, filePath)); - } - } diff --git a/plugins/hg4idea/testData/bin/hg b/plugins/hg4idea/testData/bin/hg new file mode 100755 index 000000000000..cbaef48505e0 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hg @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# +# mercurial - scalable distributed SCM +# +# Copyright 2005-2007 Matt Mackall +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# enable importing on demand to reduce startup time +try: + from mercurial import demandimport; demandimport.enable() +except ImportError: + import sys + sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" % + ' '.join(sys.path)) + sys.stderr.write("(check your install and PYTHONPATH)\n") + sys.exit(-1) + +import sys +import mercurial.util +import mercurial.dispatch + +for fp in (sys.stdin, sys.stdout, sys.stderr): + mercurial.util.set_binary(fp) + +mercurial.dispatch.run() diff --git a/plugins/hg4idea/testData/bin/hg.exe b/plugins/hg4idea/testData/bin/hg.exe new file mode 100644 index 000000000000..3d05f9bacc01 Binary files /dev/null and b/plugins/hg4idea/testData/bin/hg.exe differ diff --git a/plugins/hg4idea/testData/bin/hgext/__init__.py b/plugins/hg4idea/testData/bin/hgext/__init__.py new file mode 100644 index 000000000000..fdffa2a0fd7b --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/__init__.py @@ -0,0 +1 @@ +# placeholder diff --git a/plugins/hg4idea/testData/bin/hgext/acl.py b/plugins/hg4idea/testData/bin/hgext/acl.py new file mode 100644 index 000000000000..d8a9fa3817af --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/acl.py @@ -0,0 +1,106 @@ +# acl.py - changeset access control for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''hooks for controlling repository access + +This hook makes it possible to allow or deny write access to portions +of a repository when receiving incoming changesets. + +The authorization is matched based on the local user name on the +system where the hook runs, and not the committer of the original +changeset (since the latter is merely informative). + +The acl hook is best used along with a restricted shell like hgsh, +preventing authenticating users from doing anything other than +pushing or pulling. The hook is not safe to use if users have +interactive shell access, as they can then disable the hook. +Nor is it safe if remote users share an account, because then there +is no way to distinguish them. + +To use this hook, configure the acl extension in your hgrc like this:: + + [extensions] + acl = + + [hooks] + pretxnchangegroup.acl = python:hgext.acl.hook + + [acl] + # Check whether the source of incoming changes is in this list + # ("serve" == ssh or http, "push", "pull", "bundle") + sources = serve + +The allow and deny sections take a subtree pattern as key (with a glob +syntax by default), and a comma separated list of users as the +corresponding value. The deny list is checked before the allow list +is. :: + + [acl.allow] + # If acl.allow is not present, all users are allowed by default. + # An empty acl.allow section means no users allowed. + docs/** = doc_writer + .hgtags = release_engineer + + [acl.deny] + # If acl.deny is not present, no users are refused by default. + # An empty acl.deny section means all users allowed. + glob pattern = user4, user5 + ** = user6 +''' + +from mercurial.i18n import _ +from mercurial import util, match +import getpass, urllib + +def buildmatch(ui, repo, user, key): + '''return tuple of (match function, list enabled).''' + if not ui.has_section(key): + ui.debug('acl: %s not enabled\n' % key) + return None + + pats = [pat for pat, users in ui.configitems(key) + if users == '*' or user in users.replace(',', ' ').split()] + ui.debug('acl: %s enabled, %d entries for user %s\n' % + (key, len(pats), user)) + if pats: + return match.match(repo.root, '', pats) + return match.exact(repo.root, '', []) + + +def hook(ui, repo, hooktype, node=None, source=None, **kwargs): + if hooktype != 'pretxnchangegroup': + raise util.Abort(_('config error - hook type "%s" cannot stop ' + 'incoming changesets') % hooktype) + if source not in ui.config('acl', 'sources', 'serve').split(): + ui.debug('acl: changes have source "%s" - skipping\n' % source) + return + + user = None + if source == 'serve' and 'url' in kwargs: + url = kwargs['url'].split(':') + if url[0] == 'remote' and url[1].startswith('http'): + user = urllib.unquote(url[3]) + + if user is None: + user = getpass.getuser() + + cfg = ui.config('acl', 'config') + if cfg: + ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny']) + allow = buildmatch(ui, repo, user, 'acl.allow') + deny = buildmatch(ui, repo, user, 'acl.deny') + + for rev in xrange(repo[node], len(repo)): + ctx = repo[rev] + for f in ctx.files(): + if deny and deny(f): + ui.debug('acl: user %s denied on %s\n' % (user, f)) + raise util.Abort(_('acl: access denied for changeset %s') % ctx) + if allow and not allow(f): + ui.debug('acl: user %s not allowed on %s\n' % (user, f)) + raise util.Abort(_('acl: access denied for changeset %s') % ctx) + ui.debug('acl: allowing changeset %s\n' % ctx) diff --git a/plugins/hg4idea/testData/bin/hgext/bookmarks.py b/plugins/hg4idea/testData/bin/hgext/bookmarks.py new file mode 100644 index 000000000000..9f6c0c35fc87 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/bookmarks.py @@ -0,0 +1,334 @@ +# Mercurial extension to provide the 'hg bookmark' command +# +# Copyright 2008 David Soria Parra +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''track a line of development with movable markers + +Bookmarks are local movable markers to changesets. Every bookmark +points to a changeset identified by its hash. If you commit a +changeset that is based on a changeset that has a bookmark on it, the +bookmark shifts to the new changeset. + +It is possible to use bookmark names in every revision lookup (e.g. hg +merge, hg update). + +By default, when several bookmarks point to the same changeset, they +will all move forward together. It is possible to obtain a more +git-like experience by adding the following configuration option to +your .hgrc:: + + [bookmarks] + track.current = True + +This will cause Mercurial to track the bookmark that you are currently +using, and only update it. This is similar to git's approach to +branching. +''' + +from mercurial.i18n import _ +from mercurial.node import nullid, nullrev, hex, short +from mercurial import util, commands, repair, extensions +import os + +def write(repo): + '''Write bookmarks + + Write the given bookmark => hash dictionary to the .hg/bookmarks file + in a format equal to those of localtags. + + We also store a backup of the previous state in undo.bookmarks that + can be copied back on rollback. + ''' + refs = repo._bookmarks + if os.path.exists(repo.join('bookmarks')): + util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks')) + if repo._bookmarkcurrent not in refs: + setcurrent(repo, None) + wlock = repo.wlock() + try: + file = repo.opener('bookmarks', 'w', atomictemp=True) + for refspec, node in refs.iteritems(): + file.write("%s %s\n" % (hex(node), refspec)) + file.rename() + finally: + wlock.release() + +def setcurrent(repo, mark): + '''Set the name of the bookmark that we are currently on + + Set the name of the bookmark that we are on (hg update ). + The name is recorded in .hg/bookmarks.current + ''' + current = repo._bookmarkcurrent + if current == mark: + return + + refs = repo._bookmarks + + # do not update if we do update to a rev equal to the current bookmark + if (mark and mark not in refs and + current and refs[current] == repo.changectx('.').node()): + return + if mark not in refs: + mark = '' + wlock = repo.wlock() + try: + file = repo.opener('bookmarks.current', 'w', atomictemp=True) + file.write(mark) + file.rename() + finally: + wlock.release() + repo._bookmarkcurrent = mark + +def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None): + '''track a line of development with movable markers + + Bookmarks are pointers to certain commits that move when + committing. Bookmarks are local. They can be renamed, copied and + deleted. It is possible to use bookmark names in 'hg merge' and + 'hg update' to merge and update respectively to a given bookmark. + + You can use 'hg bookmark NAME' to set a bookmark on the working + directory's parent revision with the given name. If you specify + a revision using -r REV (where REV may be an existing bookmark), + the bookmark is assigned to that revision. + ''' + hexfn = ui.debugflag and hex or short + marks = repo._bookmarks + cur = repo.changectx('.').node() + + if rename: + if rename not in marks: + raise util.Abort(_("a bookmark of this name does not exist")) + if mark in marks and not force: + raise util.Abort(_("a bookmark of the same name already exists")) + if mark is None: + raise util.Abort(_("new bookmark name required")) + marks[mark] = marks[rename] + del marks[rename] + if repo._bookmarkcurrent == rename: + setcurrent(repo, mark) + write(repo) + return + + if delete: + if mark is None: + raise util.Abort(_("bookmark name required")) + if mark not in marks: + raise util.Abort(_("a bookmark of this name does not exist")) + if mark == repo._bookmarkcurrent: + setcurrent(repo, None) + del marks[mark] + write(repo) + return + + if mark != None: + if "\n" in mark: + raise util.Abort(_("bookmark name cannot contain newlines")) + mark = mark.strip() + if mark in marks and not force: + raise util.Abort(_("a bookmark of the same name already exists")) + if ((mark in repo.branchtags() or mark == repo.dirstate.branch()) + and not force): + raise util.Abort( + _("a bookmark cannot have the name of an existing branch")) + if rev: + marks[mark] = repo.lookup(rev) + else: + marks[mark] = repo.changectx('.').node() + setcurrent(repo, mark) + write(repo) + return + + if mark is None: + if rev: + raise util.Abort(_("bookmark name required")) + if len(marks) == 0: + ui.status(_("no bookmarks set\n")) + else: + for bmark, n in marks.iteritems(): + if ui.configbool('bookmarks', 'track.current'): + current = repo._bookmarkcurrent + prefix = (bmark == current and n == cur) and '*' or ' ' + else: + prefix = (n == cur) and '*' or ' ' + + if ui.quiet: + ui.write("%s\n" % bmark) + else: + ui.write(" %s %-25s %d:%s\n" % ( + prefix, bmark, repo.changelog.rev(n), hexfn(n))) + return + +def _revstostrip(changelog, node): + srev = changelog.rev(node) + tostrip = [srev] + saveheads = [] + for r in xrange(srev, len(changelog)): + parents = changelog.parentrevs(r) + if parents[0] in tostrip or parents[1] in tostrip: + tostrip.append(r) + if parents[1] != nullrev: + for p in parents: + if p not in tostrip and p > srev: + saveheads.append(p) + return [r for r in tostrip if r not in saveheads] + +def strip(oldstrip, ui, repo, node, backup="all"): + """Strip bookmarks if revisions are stripped using + the mercurial.strip method. This usually happens during + qpush and qpop""" + revisions = _revstostrip(repo.changelog, node) + marks = repo._bookmarks + update = [] + for mark, n in marks.iteritems(): + if repo.changelog.rev(n) in revisions: + update.append(mark) + oldstrip(ui, repo, node, backup) + if len(update) > 0: + for m in update: + marks[m] = repo.changectx('.').node() + write(repo) + +def reposetup(ui, repo): + if not repo.local(): + return + + class bookmark_repo(repo.__class__): + + @util.propertycache + def _bookmarks(self): + '''Parse .hg/bookmarks file and return a dictionary + + Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values + in the .hg/bookmarks file. They are read returned as a dictionary + with name => hash values. + ''' + try: + bookmarks = {} + for line in self.opener('bookmarks'): + sha, refspec = line.strip().split(' ', 1) + bookmarks[refspec] = super(bookmark_repo, self).lookup(sha) + except: + pass + return bookmarks + + @util.propertycache + def _bookmarkcurrent(self): + '''Get the current bookmark + + If we use gittishsh branches we have a current bookmark that + we are on. This function returns the name of the bookmark. It + is stored in .hg/bookmarks.current + ''' + mark = None + if os.path.exists(self.join('bookmarks.current')): + file = self.opener('bookmarks.current') + # No readline() in posixfile_nt, reading everything is cheap + mark = (file.readlines() or [''])[0] + if mark == '': + mark = None + file.close() + return mark + + def rollback(self): + if os.path.exists(self.join('undo.bookmarks')): + util.rename(self.join('undo.bookmarks'), self.join('bookmarks')) + return super(bookmark_repo, self).rollback() + + def lookup(self, key): + if key in self._bookmarks: + key = self._bookmarks[key] + return super(bookmark_repo, self).lookup(key) + + def _bookmarksupdate(self, parents, node): + marks = self._bookmarks + update = False + if ui.configbool('bookmarks', 'track.current'): + mark = self._bookmarkcurrent + if mark and marks[mark] in parents: + marks[mark] = node + update = True + else: + for mark, n in marks.items(): + if n in parents: + marks[mark] = node + update = True + if update: + write(self) + + def commitctx(self, ctx, error=False): + """Add a revision to the repository and + move the bookmark""" + wlock = self.wlock() # do both commit and bookmark with lock held + try: + node = super(bookmark_repo, self).commitctx(ctx, error) + if node is None: + return None + parents = self.changelog.parents(node) + if parents[1] == nullid: + parents = (parents[0],) + + self._bookmarksupdate(parents, node) + return node + finally: + wlock.release() + + def addchangegroup(self, source, srctype, url, emptyok=False): + parents = self.dirstate.parents() + + result = super(bookmark_repo, self).addchangegroup( + source, srctype, url, emptyok) + if result > 1: + # We have more heads than before + return result + node = self.changelog.tip() + + self._bookmarksupdate(parents, node) + return result + + def _findtags(self): + """Merge bookmarks with normal tags""" + (tags, tagtypes) = super(bookmark_repo, self)._findtags() + tags.update(self._bookmarks) + return (tags, tagtypes) + + if hasattr(repo, 'invalidate'): + def invalidate(self): + super(bookmark_repo, self).invalidate() + for attr in ('_bookmarks', '_bookmarkcurrent'): + if attr in self.__dict__: + delattr(repo, attr) + + repo.__class__ = bookmark_repo + +def uisetup(ui): + extensions.wrapfunction(repair, "strip", strip) + if ui.configbool('bookmarks', 'track.current'): + extensions.wrapcommand(commands.table, 'update', updatecurbookmark) + +def updatecurbookmark(orig, ui, repo, *args, **opts): + '''Set the current bookmark + + If the user updates to a bookmark we update the .hg/bookmarks.current + file. + ''' + res = orig(ui, repo, *args, **opts) + rev = opts['rev'] + if not rev and len(args) > 0: + rev = args[0] + setcurrent(repo, rev) + return res + +cmdtable = { + "bookmarks": + (bookmark, + [('f', 'force', False, _('force')), + ('r', 'rev', '', _('revision')), + ('d', 'delete', False, _('delete a given bookmark')), + ('m', 'rename', '', _('rename a given bookmark'))], + _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/bugzilla.py b/plugins/hg4idea/testData/bin/hgext/bugzilla.py new file mode 100644 index 000000000000..e94b03ff5e0d --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/bugzilla.py @@ -0,0 +1,441 @@ +# bugzilla.py - bugzilla integration for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''hooks for integrating with the Bugzilla bug tracker + +This hook extension adds comments on bugs in Bugzilla when changesets +that refer to bugs by Bugzilla ID are seen. The hook does not change +bug status. + +The hook updates the Bugzilla database directly. Only Bugzilla +installations using MySQL are supported. + +The hook relies on a Bugzilla script to send bug change notification +emails. That script changes between Bugzilla versions; the +'processmail' script used prior to 2.18 is replaced in 2.18 and +subsequent versions by 'config/sendbugmail.pl'. Note that these will +be run by Mercurial as the user pushing the change; you will need to +ensure the Bugzilla install file permissions are set appropriately. + +The extension is configured through three different configuration +sections. These keys are recognized in the [bugzilla] section: + +host + Hostname of the MySQL server holding the Bugzilla database. + +db + Name of the Bugzilla database in MySQL. Default 'bugs'. + +user + Username to use to access MySQL server. Default 'bugs'. + +password + Password to use to access MySQL server. + +timeout + Database connection timeout (seconds). Default 5. + +version + Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later, + '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior + to 2.18. + +bzuser + Fallback Bugzilla user name to record comments with, if changeset + committer cannot be found as a Bugzilla user. + +bzdir + Bugzilla install directory. Used by default notify. Default + '/var/www/html/bugzilla'. + +notify + The command to run to get Bugzilla to send bug change notification + emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id) + and 'user' (committer bugzilla email). Default depends on version; + from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl + %(id)s %(user)s". + +regexp + Regular expression to match bug IDs in changeset commit message. + Must contain one "()" group. The default expression matches 'Bug + 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug + 1234 and 5678' and variations thereof. Matching is case insensitive. + +style + The style file to use when formatting comments. + +template + Template to use when formatting comments. Overrides style if + specified. In addition to the usual Mercurial keywords, the + extension specifies:: + + {bug} The Bugzilla bug ID. + {root} The full pathname of the Mercurial repository. + {webroot} Stripped pathname of the Mercurial repository. + {hgweb} Base URL for browsing Mercurial repositories. + + Default 'changeset {node|short} in repo {root} refers ' + 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}' + +strip + The number of slashes to strip from the front of {root} to produce + {webroot}. Default 0. + +usermap + Path of file containing Mercurial committer ID to Bugzilla user ID + mappings. If specified, the file should contain one mapping per + line, "committer"="Bugzilla user". See also the [usermap] section. + +The [usermap] section is used to specify mappings of Mercurial +committer ID to Bugzilla user ID. See also [bugzilla].usermap. +"committer"="Bugzilla user" + +Finally, the [web] section supports one entry: + +baseurl + Base URL for browsing Mercurial repositories. Reference from + templates as {hgweb}. + +Activating the extension:: + + [extensions] + bugzilla = + + [hooks] + # run bugzilla hook on every change pulled or pushed in here + incoming.bugzilla = python:hgext.bugzilla.hook + +Example configuration: + +This example configuration is for a collection of Mercurial +repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2 +installation in /opt/bugzilla-3.2. :: + + [bugzilla] + host=localhost + password=XYZZY + version=3.0 + bzuser=unknown@domain.com + bzdir=/opt/bugzilla-3.2 + template=Changeset {node|short} in {root|basename}. + {hgweb}/{webroot}/rev/{node|short}\\n + {desc}\\n + strip=5 + + [web] + baseurl=http://dev.domain.com/hg + + [usermap] + user@emaildomain.com=user.name@bugzilladomain.com + +Commits add a comment to the Bugzilla bug record of the form:: + + Changeset 3b16791d6642 in repository-name. + http://dev.domain.com/hg/repository-name/rev/3b16791d6642 + + Changeset commit comment. Bug 1234. +''' + +from mercurial.i18n import _ +from mercurial.node import short +from mercurial import cmdutil, templater, util +import re, time + +MySQLdb = None + +def buglist(ids): + return '(' + ','.join(map(str, ids)) + ')' + +class bugzilla_2_16(object): + '''support for bugzilla version 2.16.''' + + def __init__(self, ui): + self.ui = ui + host = self.ui.config('bugzilla', 'host', 'localhost') + user = self.ui.config('bugzilla', 'user', 'bugs') + passwd = self.ui.config('bugzilla', 'password') + db = self.ui.config('bugzilla', 'db', 'bugs') + timeout = int(self.ui.config('bugzilla', 'timeout', 5)) + usermap = self.ui.config('bugzilla', 'usermap') + if usermap: + self.ui.readconfig(usermap, sections=['usermap']) + self.ui.note(_('connecting to %s:%s as %s, password %s\n') % + (host, db, user, '*' * len(passwd))) + self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd, + db=db, connect_timeout=timeout) + self.cursor = self.conn.cursor() + self.longdesc_id = self.get_longdesc_id() + self.user_ids = {} + self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s" + + def run(self, *args, **kwargs): + '''run a query.''' + self.ui.note(_('query: %s %s\n') % (args, kwargs)) + try: + self.cursor.execute(*args, **kwargs) + except MySQLdb.MySQLError: + self.ui.note(_('failed query: %s %s\n') % (args, kwargs)) + raise + + def get_longdesc_id(self): + '''get identity of longdesc field''' + self.run('select fieldid from fielddefs where name = "longdesc"') + ids = self.cursor.fetchall() + if len(ids) != 1: + raise util.Abort(_('unknown database schema')) + return ids[0][0] + + def filter_real_bug_ids(self, ids): + '''filter not-existing bug ids from list.''' + self.run('select bug_id from bugs where bug_id in %s' % buglist(ids)) + return sorted([c[0] for c in self.cursor.fetchall()]) + + def filter_unknown_bug_ids(self, node, ids): + '''filter bug ids from list that already refer to this changeset.''' + + self.run('''select bug_id from longdescs where + bug_id in %s and thetext like "%%%s%%"''' % + (buglist(ids), short(node))) + unknown = set(ids) + for (id,) in self.cursor.fetchall(): + self.ui.status(_('bug %d already knows about changeset %s\n') % + (id, short(node))) + unknown.discard(id) + return sorted(unknown) + + def notify(self, ids, committer): + '''tell bugzilla to send mail.''' + + self.ui.status(_('telling bugzilla to send mail:\n')) + (user, userid) = self.get_bugzilla_user(committer) + for id in ids: + self.ui.status(_(' bug %s\n') % id) + cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify) + bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla') + try: + # Backwards-compatible with old notify string, which + # took one string. This will throw with a new format + # string. + cmd = cmdfmt % id + except TypeError: + cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user} + self.ui.note(_('running notify command %s\n') % cmd) + fp = util.popen('(%s) 2>&1' % cmd) + out = fp.read() + ret = fp.close() + if ret: + self.ui.warn(out) + raise util.Abort(_('bugzilla notify command %s') % + util.explain_exit(ret)[0]) + self.ui.status(_('done\n')) + + def get_user_id(self, user): + '''look up numeric bugzilla user id.''' + try: + return self.user_ids[user] + except KeyError: + try: + userid = int(user) + except ValueError: + self.ui.note(_('looking up user %s\n') % user) + self.run('''select userid from profiles + where login_name like %s''', user) + all = self.cursor.fetchall() + if len(all) != 1: + raise KeyError(user) + userid = int(all[0][0]) + self.user_ids[user] = userid + return userid + + def map_committer(self, user): + '''map name of committer to bugzilla user name.''' + for committer, bzuser in self.ui.configitems('usermap'): + if committer.lower() == user.lower(): + return bzuser + return user + + def get_bugzilla_user(self, committer): + '''see if committer is a registered bugzilla user. Return + bugzilla username and userid if so. If not, return default + bugzilla username and userid.''' + user = self.map_committer(committer) + try: + userid = self.get_user_id(user) + except KeyError: + try: + defaultuser = self.ui.config('bugzilla', 'bzuser') + if not defaultuser: + raise util.Abort(_('cannot find bugzilla user id for %s') % + user) + userid = self.get_user_id(defaultuser) + user = defaultuser + except KeyError: + raise util.Abort(_('cannot find bugzilla user id for %s or %s') % + (user, defaultuser)) + return (user, userid) + + def add_comment(self, bugid, text, committer): + '''add comment to bug. try adding comment as committer of + changeset, otherwise as default bugzilla user.''' + (user, userid) = self.get_bugzilla_user(committer) + now = time.strftime('%Y-%m-%d %H:%M:%S') + self.run('''insert into longdescs + (bug_id, who, bug_when, thetext) + values (%s, %s, %s, %s)''', + (bugid, userid, now, text)) + self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid) + values (%s, %s, %s, %s)''', + (bugid, userid, now, self.longdesc_id)) + self.conn.commit() + +class bugzilla_2_18(bugzilla_2_16): + '''support for bugzilla 2.18 series.''' + + def __init__(self, ui): + bugzilla_2_16.__init__(self, ui) + self.default_notify = \ + "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s" + +class bugzilla_3_0(bugzilla_2_18): + '''support for bugzilla 3.0 series.''' + + def __init__(self, ui): + bugzilla_2_18.__init__(self, ui) + + def get_longdesc_id(self): + '''get identity of longdesc field''' + self.run('select id from fielddefs where name = "longdesc"') + ids = self.cursor.fetchall() + if len(ids) != 1: + raise util.Abort(_('unknown database schema')) + return ids[0][0] + +class bugzilla(object): + # supported versions of bugzilla. different versions have + # different schemas. + _versions = { + '2.16': bugzilla_2_16, + '2.18': bugzilla_2_18, + '3.0': bugzilla_3_0 + } + + _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' + r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)') + + _bz = None + + def __init__(self, ui, repo): + self.ui = ui + self.repo = repo + + def bz(self): + '''return object that knows how to talk to bugzilla version in + use.''' + + if bugzilla._bz is None: + bzversion = self.ui.config('bugzilla', 'version') + try: + bzclass = bugzilla._versions[bzversion] + except KeyError: + raise util.Abort(_('bugzilla version %s not supported') % + bzversion) + bugzilla._bz = bzclass(self.ui) + return bugzilla._bz + + def __getattr__(self, key): + return getattr(self.bz(), key) + + _bug_re = None + _split_re = None + + def find_bug_ids(self, ctx): + '''find valid bug ids that are referred to in changeset + comments and that do not already have references to this + changeset.''' + + if bugzilla._bug_re is None: + bugzilla._bug_re = re.compile( + self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re), + re.IGNORECASE) + bugzilla._split_re = re.compile(r'\D+') + start = 0 + ids = set() + while True: + m = bugzilla._bug_re.search(ctx.description(), start) + if not m: + break + start = m.end() + for id in bugzilla._split_re.split(m.group(1)): + if not id: + continue + ids.add(int(id)) + if ids: + ids = self.filter_real_bug_ids(ids) + if ids: + ids = self.filter_unknown_bug_ids(ctx.node(), ids) + return ids + + def update(self, bugid, ctx): + '''update bugzilla bug with reference to changeset.''' + + def webroot(root): + '''strip leading prefix of repo root and turn into + url-safe path.''' + count = int(self.ui.config('bugzilla', 'strip', 0)) + root = util.pconvert(root) + while count > 0: + c = root.find('/') + if c == -1: + break + root = root[c + 1:] + count -= 1 + return root + + mapfile = self.ui.config('bugzilla', 'style') + tmpl = self.ui.config('bugzilla', 'template') + t = cmdutil.changeset_templater(self.ui, self.repo, + False, None, mapfile, False) + if not mapfile and not tmpl: + tmpl = _('changeset {node|short} in repo {root} refers ' + 'to bug {bug}.\ndetails:\n\t{desc|tabindent}') + if tmpl: + tmpl = templater.parsestring(tmpl, quoted=False) + t.use_template(tmpl) + self.ui.pushbuffer() + t.show(ctx, changes=ctx.changeset(), + bug=str(bugid), + hgweb=self.ui.config('web', 'baseurl'), + root=self.repo.root, + webroot=webroot(self.repo.root)) + data = self.ui.popbuffer() + self.add_comment(bugid, data, util.email(ctx.user())) + +def hook(ui, repo, hooktype, node=None, **kwargs): + '''add comment to bugzilla for each changeset that refers to a + bugzilla bug id. only add a comment once per bug, so same change + seen multiple times does not fill bug with duplicate data.''' + try: + import MySQLdb as mysql + global MySQLdb + MySQLdb = mysql + except ImportError, err: + raise util.Abort(_('python mysql support not available: %s') % err) + + if node is None: + raise util.Abort(_('hook type %s does not pass a changeset id') % + hooktype) + try: + bz = bugzilla(ui, repo) + ctx = repo[node] + ids = bz.find_bug_ids(ctx) + if ids: + for id in ids: + bz.update(id, ctx) + bz.notify(ids, util.email(ctx.user())) + except MySQLdb.MySQLError, err: + raise util.Abort(_('database error: %s') % err[1]) + diff --git a/plugins/hg4idea/testData/bin/hgext/children.py b/plugins/hg4idea/testData/bin/hgext/children.py new file mode 100644 index 000000000000..f0df3fd5c24d --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/children.py @@ -0,0 +1,44 @@ +# Mercurial extension to provide the 'hg children' command +# +# Copyright 2007 by Intevation GmbH +# +# Author(s): +# Thomas Arendsen Hein +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to display child changesets''' + +from mercurial import cmdutil +from mercurial.commands import templateopts +from mercurial.i18n import _ + + +def children(ui, repo, file_=None, **opts): + """show the children of the given or working directory revision + + Print the children of the working directory's revisions. If a + revision is given via -r/--rev, the children of that revision will + be printed. If a file argument is given, revision in which the + file was last changed (after the working directory revision or the + argument to --rev if given) is printed. + """ + rev = opts.get('rev') + if file_: + ctx = repo.filectx(file_, changeid=rev) + else: + ctx = repo[rev] + + displayer = cmdutil.show_changeset(ui, repo, opts) + for cctx in ctx.children(): + displayer.show(cctx) + displayer.close() + +cmdtable = { + "children": + (children, + [('r', 'rev', '', _('show children of the specified revision')), + ] + templateopts, + _('hg children [-r REV] [FILE]')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/churn.py b/plugins/hg4idea/testData/bin/hgext/churn.py new file mode 100644 index 000000000000..848276caac7a --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/churn.py @@ -0,0 +1,193 @@ +# churn.py - create a graph of revisions count grouped by template +# +# Copyright 2006 Josef "Jeff" Sipek +# Copyright 2008 Alexander Solovyov +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to display statistics about repository history''' + +from mercurial.i18n import _ +from mercurial import patch, cmdutil, util, templater +import sys, os +import time, datetime + +def maketemplater(ui, repo, tmpl): + tmpl = templater.parsestring(tmpl, quoted=False) + try: + t = cmdutil.changeset_templater(ui, repo, False, None, None, False) + except SyntaxError, inst: + raise util.Abort(inst.args[0]) + t.use_template(tmpl) + return t + +def changedlines(ui, repo, ctx1, ctx2, fns): + added, removed = 0, 0 + fmatch = cmdutil.matchfiles(repo, fns) + diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) + for l in diff.split('\n'): + if l.startswith("+") and not l.startswith("+++ "): + added += 1 + elif l.startswith("-") and not l.startswith("--- "): + removed += 1 + return (added, removed) + +def countrate(ui, repo, amap, *pats, **opts): + """Calculate stats""" + if opts.get('dateformat'): + def getkey(ctx): + t, tz = ctx.date() + date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) + return date.strftime(opts['dateformat']) + else: + tmpl = opts.get('template', '{author|email}') + tmpl = maketemplater(ui, repo, tmpl) + def getkey(ctx): + ui.pushbuffer() + tmpl.show(ctx) + return ui.popbuffer() + + state = {'count': 0, 'pct': 0} + rate = {} + df = False + if opts.get('date'): + df = util.matchdate(opts['date']) + + m = cmdutil.match(repo, pats, opts) + def prep(ctx, fns): + rev = ctx.rev() + if df and not df(ctx.date()[0]): # doesn't match date format + return + + key = getkey(ctx) + key = amap.get(key, key) # alias remap + if opts.get('changesets'): + rate[key] = (rate.get(key, (0,))[0] + 1, 0) + else: + parents = ctx.parents() + if len(parents) > 1: + ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) + return + + ctx1 = parents[0] + lines = changedlines(ui, repo, ctx1, ctx, fns) + rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] + + if opts.get('progress'): + state['count'] += 1 + newpct = int(100.0 * state['count'] / max(len(repo), 1)) + if state['pct'] < newpct: + state['pct'] = newpct + ui.write("\r" + _("generating stats: %d%%") % state['pct']) + sys.stdout.flush() + + for ctx in cmdutil.walkchangerevs(repo, m, opts, prep): + continue + + if opts.get('progress'): + ui.write("\r") + sys.stdout.flush() + + return rate + + +def churn(ui, repo, *pats, **opts): + '''histogram of changes to the repository + + This command will display a histogram representing the number + of changed lines or revisions, grouped according to the given + template. The default template will group changes by author. + The --dateformat option may be used to group the results by + date instead. + + Statistics are based on the number of changed lines, or + alternatively the number of matching revisions if the + --changesets option is specified. + + Examples:: + + # display count of changed lines for every committer + hg churn -t '{author|email}' + + # display daily activity graph + hg churn -f '%H' -s -c + + # display activity of developers by month + hg churn -f '%Y-%m' -s -c + + # display count of lines changed in every year + hg churn -f '%Y' -s + + It is possible to map alternate email addresses to a main address + by providing a file using the following format:: + + + + Such a file may be specified with the --aliases option, otherwise + a .hgchurn file will be looked for in the working directory root. + ''' + def pad(s, l): + return (s + " " * l)[:l] + + amap = {} + aliases = opts.get('aliases') + if not aliases and os.path.exists(repo.wjoin('.hgchurn')): + aliases = repo.wjoin('.hgchurn') + if aliases: + for l in open(aliases, "r"): + l = l.strip() + alias, actual = l.split() + amap[alias] = actual + + rate = countrate(ui, repo, amap, *pats, **opts).items() + if not rate: + return + + sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None) + rate.sort(key=sortkey) + + # Be careful not to have a zero maxcount (issue833) + maxcount = float(max(sum(v) for k, v in rate)) or 1.0 + maxname = max(len(k) for k, v in rate) + + ttywidth = util.termwidth() + ui.debug("assuming %i character terminal\n" % ttywidth) + width = ttywidth - maxname - 2 - 2 - 2 + + if opts.get('diffstat'): + width -= 15 + def format(name, (added, removed)): + return "%s %15s %s%s\n" % (pad(name, maxname), + '+%d/-%d' % (added, removed), + '+' * charnum(added), + '-' * charnum(removed)) + else: + width -= 6 + def format(name, count): + return "%s %6d %s\n" % (pad(name, maxname), sum(count), + '*' * charnum(sum(count))) + + def charnum(count): + return int(round(count * width / maxcount)) + + for name, count in rate: + ui.write(format(name, count)) + + +cmdtable = { + "churn": + (churn, + [('r', 'rev', [], _('count rate for the specified revision or range')), + ('d', 'date', '', _('count rate for revisions matching date spec')), + ('t', 'template', '{author|email}', + _('template to group changesets')), + ('f', 'dateformat', '', + _('strftime-compatible format for grouping by date')), + ('c', 'changesets', False, _('count rate by number of changesets')), + ('s', 'sort', False, _('sort by key (default: sort by count)')), + ('', 'diffstat', False, _('display added/removed lines separately')), + ('', 'aliases', '', _('file with email aliases')), + ('', 'progress', None, _('show progress'))], + _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")), +} diff --git a/plugins/hg4idea/testData/bin/hgext/color.py b/plugins/hg4idea/testData/bin/hgext/color.py new file mode 100644 index 000000000000..6e414898bd52 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/color.py @@ -0,0 +1,374 @@ +# color.py color output for the status and qseries commands +# +# Copyright (C) 2007 Kevin Christen +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +'''colorize output from some commands + +This extension modifies the status and resolve commands to add color to their +output to reflect file status, the qseries command to add color to reflect +patch status (applied, unapplied, missing), and to diff-related +commands to highlight additions, removals, diff headers, and trailing +whitespace. + +Other effects in addition to color, like bold and underlined text, are +also available. Effects are rendered with the ECMA-48 SGR control +function (aka ANSI escape codes). This module also provides the +render_text function, which can be used to add effects to any text. + +Default effects may be overridden from the .hgrc file:: + + [color] + status.modified = blue bold underline red_background + status.added = green bold + status.removed = red bold blue_background + status.deleted = cyan bold underline + status.unknown = magenta bold underline + status.ignored = black bold + + # 'none' turns off all effects + status.clean = none + status.copied = none + + qseries.applied = blue bold underline + qseries.unapplied = black bold + qseries.missing = red bold + + diff.diffline = bold + diff.extended = cyan bold + diff.file_a = red bold + diff.file_b = green bold + diff.hunk = magenta + diff.deleted = red + diff.inserted = green + diff.changed = white + diff.trailingwhitespace = bold red_background + + resolve.unresolved = red bold + resolve.resolved = green bold + + bookmarks.current = green +''' + +import os, sys + +from mercurial import cmdutil, commands, extensions +from mercurial.i18n import _ + +# start and stop parameters for effects +_effect_params = {'none': 0, + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'bold': 1, + 'italic': 3, + 'underline': 4, + 'inverse': 7, + 'black_background': 40, + 'red_background': 41, + 'green_background': 42, + 'yellow_background': 43, + 'blue_background': 44, + 'purple_background': 45, + 'cyan_background': 46, + 'white_background': 47} + +def render_effects(text, effects): + 'Wrap text in commands to turn on each effect.' + start = [str(_effect_params[e]) for e in ['none'] + effects] + start = '\033[' + ';'.join(start) + 'm' + stop = '\033[' + str(_effect_params['none']) + 'm' + return ''.join([start, text, stop]) + +def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts): + '''run a status-like command with colorized output''' + delimiter = opts.get('print0') and '\0' or '\n' + + nostatus = opts.get('no_status') + opts['no_status'] = False + # run original command and capture its output + ui.pushbuffer() + retval = orig(ui, repo, *pats, **opts) + # filter out empty strings + lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line] + + if nostatus: + lines = [l[2:] for l in lines_with_status] + else: + lines = lines_with_status + + # apply color to output and display it + for i in xrange(len(lines)): + try: + status = abbreviations[lines_with_status[i][0]] + except KeyError: + # Ignore lines with invalid codes, especially in the case of + # of unknown filenames containing newlines (issue2036). + pass + else: + effects = effectdefs[status] + if effects: + lines[i] = render_effects(lines[i], effects) + ui.write(lines[i] + delimiter) + return retval + + +_status_abbreviations = { 'M': 'modified', + 'A': 'added', + 'R': 'removed', + '!': 'deleted', + '?': 'unknown', + 'I': 'ignored', + 'C': 'clean', + ' ': 'copied', } + +_status_effects = { 'modified': ['blue', 'bold'], + 'added': ['green', 'bold'], + 'removed': ['red', 'bold'], + 'deleted': ['cyan', 'bold', 'underline'], + 'unknown': ['magenta', 'bold', 'underline'], + 'ignored': ['black', 'bold'], + 'clean': ['none'], + 'copied': ['none'], } + +def colorstatus(orig, ui, repo, *pats, **opts): + '''run the status command with colored output''' + return _colorstatuslike(_status_abbreviations, _status_effects, + orig, ui, repo, *pats, **opts) + + +_resolve_abbreviations = { 'U': 'unresolved', + 'R': 'resolved', } + +_resolve_effects = { 'unresolved': ['red', 'bold'], + 'resolved': ['green', 'bold'], } + +def colorresolve(orig, ui, repo, *pats, **opts): + '''run the resolve command with colored output''' + if not opts.get('list'): + # only colorize for resolve -l + return orig(ui, repo, *pats, **opts) + return _colorstatuslike(_resolve_abbreviations, _resolve_effects, + orig, ui, repo, *pats, **opts) + + +_bookmark_effects = { 'current': ['green'] } + +def colorbookmarks(orig, ui, repo, *pats, **opts): + def colorize(orig, s): + lines = s.split('\n') + for i, line in enumerate(lines): + if line.startswith(" *"): + lines[i] = render_effects(line, _bookmark_effects['current']) + orig('\n'.join(lines)) + oldwrite = extensions.wrapfunction(ui, 'write', colorize) + try: + orig(ui, repo, *pats, **opts) + finally: + ui.write = oldwrite + +def colorqseries(orig, ui, repo, *dummy, **opts): + '''run the qseries command with colored output''' + ui.pushbuffer() + retval = orig(ui, repo, **opts) + patchlines = ui.popbuffer().splitlines() + patchnames = repo.mq.series + + for patch, patchname in zip(patchlines, patchnames): + if opts['missing']: + effects = _patch_effects['missing'] + # Determine if patch is applied. + elif [applied for applied in repo.mq.applied + if patchname == applied.name]: + effects = _patch_effects['applied'] + else: + effects = _patch_effects['unapplied'] + + patch = patch.replace(patchname, render_effects(patchname, effects), 1) + ui.write(patch + '\n') + return retval + +_patch_effects = { 'applied': ['blue', 'bold', 'underline'], + 'missing': ['red', 'bold'], + 'unapplied': ['black', 'bold'], } +def colorwrap(orig, *args): + '''wrap ui.write for colored diff output''' + def _colorize(s): + lines = s.split('\n') + for i, line in enumerate(lines): + stripline = line + if line and line[0] in '+-': + # highlight trailing whitespace, but only in changed lines + stripline = line.rstrip() + for prefix, style in _diff_prefixes: + if stripline.startswith(prefix): + lines[i] = render_effects(stripline, _diff_effects[style]) + break + if line != stripline: + lines[i] += render_effects( + line[len(stripline):], _diff_effects['trailingwhitespace']) + return '\n'.join(lines) + orig(*[_colorize(s) for s in args]) + +def colorshowpatch(orig, self, node): + '''wrap cmdutil.changeset_printer.showpatch with colored output''' + oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap) + try: + orig(self, node) + finally: + self.ui.write = oldwrite + +def colordiffstat(orig, s): + lines = s.split('\n') + for i, line in enumerate(lines): + if line and line[-1] in '+-': + name, graph = line.rsplit(' ', 1) + graph = graph.replace('-', + render_effects('-', _diff_effects['deleted'])) + graph = graph.replace('+', + render_effects('+', _diff_effects['inserted'])) + lines[i] = ' '.join([name, graph]) + orig('\n'.join(lines)) + +def colordiff(orig, ui, repo, *pats, **opts): + '''run the diff command with colored output''' + if opts.get('stat'): + wrapper = colordiffstat + else: + wrapper = colorwrap + oldwrite = extensions.wrapfunction(ui, 'write', wrapper) + try: + orig(ui, repo, *pats, **opts) + finally: + ui.write = oldwrite + +def colorchurn(orig, ui, repo, *pats, **opts): + '''run the churn command with colored output''' + if not opts.get('diffstat'): + return orig(ui, repo, *pats, **opts) + oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat) + try: + orig(ui, repo, *pats, **opts) + finally: + ui.write = oldwrite + +_diff_prefixes = [('diff', 'diffline'), + ('copy', 'extended'), + ('rename', 'extended'), + ('old', 'extended'), + ('new', 'extended'), + ('deleted', 'extended'), + ('---', 'file_a'), + ('+++', 'file_b'), + ('@', 'hunk'), + ('-', 'deleted'), + ('+', 'inserted')] + +_diff_effects = {'diffline': ['bold'], + 'extended': ['cyan', 'bold'], + 'file_a': ['red', 'bold'], + 'file_b': ['green', 'bold'], + 'hunk': ['magenta'], + 'deleted': ['red'], + 'inserted': ['green'], + 'changed': ['white'], + 'trailingwhitespace': ['bold', 'red_background']} + +def extsetup(ui): + '''Initialize the extension.''' + _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects) + _setupcmd(ui, 'incoming', commands.table, None, _diff_effects) + _setupcmd(ui, 'log', commands.table, None, _diff_effects) + _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects) + _setupcmd(ui, 'tip', commands.table, None, _diff_effects) + _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects) + _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects) + + try: + mq = extensions.find('mq') + _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects) + _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects) + except KeyError: + mq = None + + try: + rec = extensions.find('record') + _setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects) + except KeyError: + rec = None + + if mq and rec: + _setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects) + try: + churn = extensions.find('churn') + _setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects) + except KeyError: + churn = None + + try: + bookmarks = extensions.find('bookmarks') + _setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks, + _bookmark_effects) + except KeyError: + # The bookmarks extension is not enabled + pass + +def _setupcmd(ui, cmd, table, func, effectsmap): + '''patch in command to command table and load effect map''' + def nocolor(orig, *args, **opts): + + if (opts['no_color'] or opts['color'] == 'never' or + (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb' + or not sys.__stdout__.isatty()))): + del opts['no_color'] + del opts['color'] + return orig(*args, **opts) + + oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer, + 'showpatch', colorshowpatch) + del opts['no_color'] + del opts['color'] + try: + if func is not None: + return func(orig, *args, **opts) + return orig(*args, **opts) + finally: + cmdutil.changeset_printer.showpatch = oldshowpatch + + entry = extensions.wrapcommand(table, cmd, nocolor) + entry[1].extend([ + ('', 'color', 'auto', _("when to colorize (always, auto, or never)")), + ('', 'no-color', None, _("don't colorize output (DEPRECATED)")), + ]) + + for status in effectsmap: + configkey = cmd + '.' + status + effects = ui.configlist('color', configkey) + if effects: + good = [] + for e in effects: + if e in _effect_params: + good.append(e) + else: + ui.warn(_("ignoring unknown color/effect %r " + "(configured in color.%s)\n") + % (e, configkey)) + effectsmap[status] = good diff --git a/plugins/hg4idea/testData/bin/hgext/convert/__init__.py b/plugins/hg4idea/testData/bin/hgext/convert/__init__.py new file mode 100644 index 000000000000..a11fbb065eb8 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/__init__.py @@ -0,0 +1,295 @@ +# convert.py Foreign SCM converter +# +# Copyright 2005-2007 Matt Mackall +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''import revisions from foreign VCS repositories into Mercurial''' + +import convcmd +import cvsps +import subversion +from mercurial import commands +from mercurial.i18n import _ + +# Commands definition was moved elsewhere to ease demandload job. + +def convert(ui, src, dest=None, revmapfile=None, **opts): + """convert a foreign SCM repository to a Mercurial one. + + Accepted source formats [identifiers]: + + - Mercurial [hg] + - CVS [cvs] + - Darcs [darcs] + - git [git] + - Subversion [svn] + - Monotone [mtn] + - GNU Arch [gnuarch] + - Bazaar [bzr] + - Perforce [p4] + + Accepted destination formats [identifiers]: + + - Mercurial [hg] + - Subversion [svn] (history on branches is not preserved) + + If no revision is given, all revisions will be converted. + Otherwise, convert will only import up to the named revision + (given in a format understood by the source). + + If no destination directory name is specified, it defaults to the + basename of the source with '-hg' appended. If the destination + repository doesn't exist, it will be created. + + By default, all sources except Mercurial will use --branchsort. + Mercurial uses --sourcesort to preserve original revision numbers + order. Sort modes have the following effects: + + --branchsort convert from parent to child revision when possible, + which means branches are usually converted one after + the other. It generates more compact repositories. + + --datesort sort revisions by date. Converted repositories have + good-looking changelogs but are often an order of + magnitude larger than the same ones generated by + --branchsort. + + --sourcesort try to preserve source revisions order, only + supported by Mercurial sources. + + If isn't given, it will be put in a default location + (/.hg/shamap by default). The is a simple text file + that maps each source commit ID to the destination ID for that + revision, like so:: + + + + If the file doesn't exist, it's automatically created. It's + updated on each commit copied, so convert-repo can be interrupted + and can be run repeatedly to copy new commits. + + The [username mapping] file is a simple text file that maps each + source commit author to a destination commit author. It is handy + for source SCMs that use unix logins to identify authors (eg: + CVS). One line per author mapping and the line format is: + srcauthor=whatever string you want + + The filemap is a file that allows filtering and remapping of files + and directories. Comment lines start with '#'. Each line can + contain one of the following directives:: + + include path/to/file + + exclude path/to/file + + rename from/file to/file + + The 'include' directive causes a file, or all files under a + directory, to be included in the destination repository, and the + exclusion of all other files and directories not explicitly + included. The 'exclude' directive causes files or directories to + be omitted. The 'rename' directive renames a file or directory. To + rename from a subdirectory into the root of the repository, use + '.' as the path to rename to. + + The splicemap is a file that allows insertion of synthetic + history, letting you specify the parents of a revision. This is + useful if you want to e.g. give a Subversion merge two parents, or + graft two disconnected series of history together. Each entry + contains a key, followed by a space, followed by one or two + comma-separated values. The key is the revision ID in the source + revision control system whose parents should be modified (same + format as a key in .hg/shamap). The values are the revision IDs + (in either the source or destination revision control system) that + should be used as the new parents for that node. For example, if + you have merged "release-1.0" into "trunk", then you should + specify the revision on "trunk" as the first parent and the one on + the "release-1.0" branch as the second. + + The branchmap is a file that allows you to rename a branch when it is + being brought in from whatever external repository. When used in + conjunction with a splicemap, it allows for a powerful combination + to help fix even the most badly mismanaged repositories and turn them + into nicely structured Mercurial repositories. The branchmap contains + lines of the form "original_branch_name new_branch_name". + "original_branch_name" is the name of the branch in the source + repository, and "new_branch_name" is the name of the branch is the + destination repository. This can be used to (for instance) move code + in one repository from "default" to a named branch. + + Mercurial Source + ---------------- + + --config convert.hg.ignoreerrors=False (boolean) + ignore integrity errors when reading. Use it to fix Mercurial + repositories with missing revlogs, by converting from and to + Mercurial. + --config convert.hg.saverev=False (boolean) + store original revision ID in changeset (forces target IDs to + change) + --config convert.hg.startrev=0 (hg revision identifier) + convert start revision and its descendants + + CVS Source + ---------- + + CVS source will use a sandbox (i.e. a checked-out copy) from CVS + to indicate the starting point of what will be converted. Direct + access to the repository files is not needed, unless of course the + repository is :local:. The conversion uses the top level directory + in the sandbox to find the CVS repository, and then uses CVS rlog + commands to find files to convert. This means that unless a + filemap is given, all files under the starting directory will be + converted, and that any directory reorganization in the CVS + sandbox is ignored. + + The options shown are the defaults. + + --config convert.cvsps.cache=True (boolean) + Set to False to disable remote log caching, for testing and + debugging purposes. + --config convert.cvsps.fuzz=60 (integer) + Specify the maximum time (in seconds) that is allowed between + commits with identical user and log message in a single + changeset. When very large files were checked in as part of a + changeset then the default may not be long enough. + --config convert.cvsps.mergeto='{{mergetobranch ([-\\w]+)}}' + Specify a regular expression to which commit log messages are + matched. If a match occurs, then the conversion process will + insert a dummy revision merging the branch on which this log + message occurs to the branch indicated in the regex. + --config convert.cvsps.mergefrom='{{mergefrombranch ([-\\w]+)}}' + Specify a regular expression to which commit log messages are + matched. If a match occurs, then the conversion process will + add the most recent revision on the branch indicated in the + regex as the second parent of the changeset. + --config hook.cvslog + Specify a Python function to be called at the end of gathering + the CVS log. The function is passed a list with the log entries, + and can modify the entries in-place, or add or delete them. + --config hook.cvschangesets + Specify a Python function to be called after the changesets + are calculated from the the CVS log. The function is passed + a list with the changeset entries, and can modify the changesets + in-place, or add or delete them. + + An additional "debugcvsps" Mercurial command allows the builtin + changeset merging code to be run without doing a conversion. Its + parameters and output are similar to that of cvsps 2.1. Please see + the command help for more details. + + Subversion Source + ----------------- + + Subversion source detects classical trunk/branches/tags layouts. + By default, the supplied "svn://repo/path/" source URL is + converted as a single branch. If "svn://repo/path/trunk" exists it + replaces the default branch. If "svn://repo/path/branches" exists, + its subdirectories are listed as possible branches. If + "svn://repo/path/tags" exists, it is looked for tags referencing + converted branches. Default "trunk", "branches" and "tags" values + can be overridden with following options. Set them to paths + relative to the source URL, or leave them blank to disable auto + detection. + + --config convert.svn.branches=branches (directory name) + specify the directory containing branches + --config convert.svn.tags=tags (directory name) + specify the directory containing tags + --config convert.svn.trunk=trunk (directory name) + specify the name of the trunk branch + + Source history can be retrieved starting at a specific revision, + instead of being integrally converted. Only single branch + conversions are supported. + + --config convert.svn.startrev=0 (svn revision number) + specify start Subversion revision. + + Perforce Source + --------------- + + The Perforce (P4) importer can be given a p4 depot path or a + client specification as source. It will convert all files in the + source to a flat Mercurial repository, ignoring labels, branches + and integrations. Note that when a depot path is given you then + usually should specify a target directory, because otherwise the + target may be named ...-hg. + + It is possible to limit the amount of source history to be + converted by specifying an initial Perforce revision. + + --config convert.p4.startrev=0 (perforce changelist number) + specify initial Perforce revision. + + Mercurial Destination + --------------------- + + --config convert.hg.clonebranches=False (boolean) + dispatch source branches in separate clones. + --config convert.hg.tagsbranch=default (branch name) + tag revisions branch name + --config convert.hg.usebranchnames=True (boolean) + preserve branch names + + """ + return convcmd.convert(ui, src, dest, revmapfile, **opts) + +def debugsvnlog(ui, **opts): + return subversion.debugsvnlog(ui, **opts) + +def debugcvsps(ui, *args, **opts): + '''create changeset information from CVS + + This command is intended as a debugging tool for the CVS to + Mercurial converter, and can be used as a direct replacement for + cvsps. + + Hg debugcvsps reads the CVS rlog for current directory (or any + named directory) in the CVS repository, and converts the log to a + series of changesets based on matching commit log entries and + dates.''' + return cvsps.debugcvsps(ui, *args, **opts) + +commands.norepo += " convert debugsvnlog debugcvsps" + +cmdtable = { + "convert": + (convert, + [('A', 'authors', '', _('username mapping filename')), + ('d', 'dest-type', '', _('destination repository type')), + ('', 'filemap', '', _('remap file names using contents of file')), + ('r', 'rev', '', _('import up to target revision REV')), + ('s', 'source-type', '', _('source repository type')), + ('', 'splicemap', '', _('splice synthesized history into place')), + ('', 'branchmap', '', _('change branch names while converting')), + ('', 'branchsort', None, _('try to sort changesets by branches')), + ('', 'datesort', None, _('try to sort changesets by date')), + ('', 'sourcesort', None, _('preserve source changesets order'))], + _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')), + "debugsvnlog": + (debugsvnlog, + [], + 'hg debugsvnlog'), + "debugcvsps": + (debugcvsps, + [ + # Main options shared with cvsps-2.1 + ('b', 'branches', [], _('only return changes on specified branches')), + ('p', 'prefix', '', _('prefix to remove from file names')), + ('r', 'revisions', [], + _('only return changes after or between specified tags')), + ('u', 'update-cache', None, _("update cvs log cache")), + ('x', 'new-cache', None, _("create new cvs log cache")), + ('z', 'fuzz', 60, _('set commit time fuzz in seconds')), + ('', 'root', '', _('specify cvsroot')), + # Options specific to builtin cvsps + ('', 'parents', '', _('show parent changesets')), + ('', 'ancestors', '', _('show current changeset in ancestor branches')), + # Options that are ignored for compatibility with cvsps-2.1 + ('A', 'cvs-direct', None, _('ignored for compatibility')), + ], + _('hg debugcvsps [OPTION]... [PATH]...')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/convert/bzr.py b/plugins/hg4idea/testData/bin/hgext/convert/bzr.py new file mode 100644 index 000000000000..7434d31eb33d --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/bzr.py @@ -0,0 +1,262 @@ +# bzr.py - bzr support for the convert extension +# +# Copyright 2008, 2009 Marek Kubica and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# This module is for handling 'bzr', that was formerly known as Bazaar-NG; +# it cannot access 'bar' repositories, but they were never used very much + +import os +from mercurial import demandimport +# these do not work with demandimport, blacklist +demandimport.ignore.extend([ + 'bzrlib.transactions', + 'bzrlib.urlutils', + 'ElementPath', + ]) + +from mercurial.i18n import _ +from mercurial import util +from common import NoRepo, commit, converter_source + +try: + # bazaar imports + from bzrlib import branch, revision, errors + from bzrlib.revisionspec import RevisionSpec +except ImportError: + pass + +supportedkinds = ('file', 'symlink') + +class bzr_source(converter_source): + """Reads Bazaar repositories by using the Bazaar Python libraries""" + + def __init__(self, ui, path, rev=None): + super(bzr_source, self).__init__(ui, path, rev=rev) + + if not os.path.exists(os.path.join(path, '.bzr')): + raise NoRepo(_('%s does not look like a Bazaar repository') + % path) + + try: + # access bzrlib stuff + branch + except NameError: + raise NoRepo(_('Bazaar modules could not be loaded')) + + path = os.path.abspath(path) + self._checkrepotype(path) + self.branch = branch.Branch.open(path) + self.sourcerepo = self.branch.repository + self._parentids = {} + + def _checkrepotype(self, path): + # Lightweight checkouts detection is informational but probably + # fragile at API level. It should not terminate the conversion. + try: + from bzrlib import bzrdir + dir = bzrdir.BzrDir.open_containing(path)[0] + try: + tree = dir.open_workingtree(recommend_upgrade=False) + branch = tree.branch + except (errors.NoWorkingTree, errors.NotLocalUrl), e: + tree = None + branch = dir.open_branch() + if (tree is not None and tree.bzrdir.root_transport.base != + branch.bzrdir.root_transport.base): + self.ui.warn(_('warning: lightweight checkouts may cause ' + 'conversion failures, try with a regular ' + 'branch instead.\n')) + except: + self.ui.note(_('bzr source type could not be determined\n')) + + def before(self): + """Before the conversion begins, acquire a read lock + for all the operations that might need it. Fortunately + read locks don't block other reads or writes to the + repository, so this shouldn't have any impact on the usage of + the source repository. + + The alternative would be locking on every operation that + needs locks (there are currently two: getting the file and + getting the parent map) and releasing immediately after, + but this approach can take even 40% longer.""" + self.sourcerepo.lock_read() + + def after(self): + self.sourcerepo.unlock() + + def getheads(self): + if not self.rev: + return [self.branch.last_revision()] + try: + r = RevisionSpec.from_string(self.rev) + info = r.in_history(self.branch) + except errors.BzrError: + raise util.Abort(_('%s is not a valid revision in current branch') + % self.rev) + return [info.rev_id] + + def getfile(self, name, rev): + revtree = self.sourcerepo.revision_tree(rev) + fileid = revtree.path2id(name.decode(self.encoding or 'utf-8')) + kind = None + if fileid is not None: + kind = revtree.kind(fileid) + if kind not in supportedkinds: + # the file is not available anymore - was deleted + raise IOError(_('%s is not available in %s anymore') % + (name, rev)) + if kind == 'symlink': + target = revtree.get_symlink_target(fileid) + if target is None: + raise util.Abort(_('%s.%s symlink has no target') + % (name, rev)) + return target + else: + sio = revtree.get_file(fileid) + return sio.read() + + def getmode(self, name, rev): + return self._modecache[(name, rev)] + + def getchanges(self, version): + # set up caches: modecache and revtree + self._modecache = {} + self._revtree = self.sourcerepo.revision_tree(version) + # get the parentids from the cache + parentids = self._parentids.pop(version) + # only diff against first parent id + prevtree = self.sourcerepo.revision_tree(parentids[0]) + return self._gettreechanges(self._revtree, prevtree) + + def getcommit(self, version): + rev = self.sourcerepo.get_revision(version) + # populate parent id cache + if not rev.parent_ids: + parents = [] + self._parentids[version] = (revision.NULL_REVISION,) + else: + parents = self._filterghosts(rev.parent_ids) + self._parentids[version] = parents + + return commit(parents=parents, + date='%d %d' % (rev.timestamp, -rev.timezone), + author=self.recode(rev.committer), + # bzr returns bytestrings or unicode, depending on the content + desc=self.recode(rev.message), + rev=version) + + def gettags(self): + if not self.branch.supports_tags(): + return {} + tagdict = self.branch.tags.get_tag_dict() + bytetags = {} + for name, rev in tagdict.iteritems(): + bytetags[self.recode(name)] = rev + return bytetags + + def getchangedfiles(self, rev, i): + self._modecache = {} + curtree = self.sourcerepo.revision_tree(rev) + if i is not None: + parentid = self._parentids[rev][i] + else: + # no parent id, get the empty revision + parentid = revision.NULL_REVISION + + prevtree = self.sourcerepo.revision_tree(parentid) + changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]] + return changes + + def _gettreechanges(self, current, origin): + revid = current._revision_id + changes = [] + renames = {} + for (fileid, paths, changed_content, versioned, parent, name, + kind, executable) in current.iter_changes(origin): + + if paths[0] == u'' or paths[1] == u'': + # ignore changes to tree root + continue + + # bazaar tracks directories, mercurial does not, so + # we have to rename the directory contents + if kind[1] == 'directory': + if kind[0] not in (None, 'directory'): + # Replacing 'something' with a directory, record it + # so it can be removed. + changes.append((self.recode(paths[0]), revid)) + + if None not in paths and paths[0] != paths[1]: + # neither an add nor an delete - a move + # rename all directory contents manually + subdir = origin.inventory.path2id(paths[0]) + # get all child-entries of the directory + for name, entry in origin.inventory.iter_entries(subdir): + # hg does not track directory renames + if entry.kind == 'directory': + continue + frompath = self.recode(paths[0] + '/' + name) + topath = self.recode(paths[1] + '/' + name) + # register the files as changed + changes.append((frompath, revid)) + changes.append((topath, revid)) + # add to mode cache + mode = ((entry.executable and 'x') + or (entry.kind == 'symlink' and 's') + or '') + self._modecache[(topath, revid)] = mode + # register the change as move + renames[topath] = frompath + + # no futher changes, go to the next change + continue + + # we got unicode paths, need to convert them + path, topath = [self.recode(part) for part in paths] + + if topath is None: + # file deleted + changes.append((path, revid)) + continue + + # renamed + if path and path != topath: + renames[topath] = path + changes.append((path, revid)) + + # populate the mode cache + kind, executable = [e[1] for e in (kind, executable)] + mode = ((executable and 'x') or (kind == 'symlink' and 'l') + or '') + self._modecache[(topath, revid)] = mode + changes.append((topath, revid)) + + return changes, renames + + def _filterghosts(self, ids): + """Filters out ghost revisions which hg does not support, see + + """ + parentmap = self.sourcerepo.get_parent_map(ids) + parents = tuple([parent for parent in ids if parent in parentmap]) + return parents + + def recode(self, s, encoding=None): + """This version of recode tries to encode unicode to bytecode, + and preferably using the UTF-8 codec. + Other types than Unicode are silently returned, this is by + intention, e.g. the None-type is not going to be encoded but instead + just passed through + """ + if not encoding: + encoding = self.encoding or 'utf-8' + + if isinstance(s, unicode): + return s.encode(encoding) + else: + # leave it alone + return s diff --git a/plugins/hg4idea/testData/bin/hgext/convert/common.py b/plugins/hg4idea/testData/bin/hgext/convert/common.py new file mode 100644 index 000000000000..3c57578e1c60 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/common.py @@ -0,0 +1,394 @@ +# common.py - common code for the convert extension +# +# Copyright 2005-2009 Matt Mackall and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import base64, errno +import os +import cPickle as pickle +from mercurial import util +from mercurial.i18n import _ + +def encodeargs(args): + def encodearg(s): + lines = base64.encodestring(s) + lines = [l.splitlines()[0] for l in lines] + return ''.join(lines) + + s = pickle.dumps(args) + return encodearg(s) + +def decodeargs(s): + s = base64.decodestring(s) + return pickle.loads(s) + +class MissingTool(Exception): + pass + +def checktool(exe, name=None, abort=True): + name = name or exe + if not util.find_exe(exe): + exc = abort and util.Abort or MissingTool + raise exc(_('cannot find required "%s" tool') % name) + +class NoRepo(Exception): + pass + +SKIPREV = 'SKIP' + +class commit(object): + def __init__(self, author, date, desc, parents, branch=None, rev=None, + extra={}, sortkey=None): + self.author = author or 'unknown' + self.date = date or '0 0' + self.desc = desc + self.parents = parents + self.branch = branch + self.rev = rev + self.extra = extra + self.sortkey = sortkey + +class converter_source(object): + """Conversion source interface""" + + def __init__(self, ui, path=None, rev=None): + """Initialize conversion source (or raise NoRepo("message") + exception if path is not a valid repository)""" + self.ui = ui + self.path = path + self.rev = rev + + self.encoding = 'utf-8' + + def before(self): + pass + + def after(self): + pass + + def setrevmap(self, revmap): + """set the map of already-converted revisions""" + pass + + def getheads(self): + """Return a list of this repository's heads""" + raise NotImplementedError() + + def getfile(self, name, rev): + """Return file contents as a string. rev is the identifier returned + by a previous call to getchanges(). Raise IOError to indicate that + name was deleted in rev. + """ + raise NotImplementedError() + + def getmode(self, name, rev): + """Return file mode, eg. '', 'x', or 'l'. rev is the identifier + returned by a previous call to getchanges(). + """ + raise NotImplementedError() + + def getchanges(self, version): + """Returns a tuple of (files, copies). + + files is a sorted list of (filename, id) tuples for all files + changed between version and its first parent returned by + getcommit(). id is the source revision id of the file. + + copies is a dictionary of dest: source + """ + raise NotImplementedError() + + def getcommit(self, version): + """Return the commit object for version""" + raise NotImplementedError() + + def gettags(self): + """Return the tags as a dictionary of name: revision + + Tag names must be UTF-8 strings. + """ + raise NotImplementedError() + + def recode(self, s, encoding=None): + if not encoding: + encoding = self.encoding or 'utf-8' + + if isinstance(s, unicode): + return s.encode("utf-8") + try: + return s.decode(encoding).encode("utf-8") + except: + try: + return s.decode("latin-1").encode("utf-8") + except: + return s.decode(encoding, "replace").encode("utf-8") + + def getchangedfiles(self, rev, i): + """Return the files changed by rev compared to parent[i]. + + i is an index selecting one of the parents of rev. The return + value should be the list of files that are different in rev and + this parent. + + If rev has no parents, i is None. + + This function is only needed to support --filemap + """ + raise NotImplementedError() + + def converted(self, rev, sinkrev): + '''Notify the source that a revision has been converted.''' + pass + + def hasnativeorder(self): + """Return true if this source has a meaningful, native revision + order. For instance, Mercurial revisions are store sequentially + while there is no such global ordering with Darcs. + """ + return False + + def lookuprev(self, rev): + """If rev is a meaningful revision reference in source, return + the referenced identifier in the same format used by getcommit(). + return None otherwise. + """ + return None + +class converter_sink(object): + """Conversion sink (target) interface""" + + def __init__(self, ui, path): + """Initialize conversion sink (or raise NoRepo("message") + exception if path is not a valid repository) + + created is a list of paths to remove if a fatal error occurs + later""" + self.ui = ui + self.path = path + self.created = [] + + def getheads(self): + """Return a list of this repository's heads""" + raise NotImplementedError() + + def revmapfile(self): + """Path to a file that will contain lines + source_rev_id sink_rev_id + mapping equivalent revision identifiers for each system.""" + raise NotImplementedError() + + def authorfile(self): + """Path to a file that will contain lines + srcauthor=dstauthor + mapping equivalent authors identifiers for each system.""" + return None + + def putcommit(self, files, copies, parents, commit, source, revmap): + """Create a revision with all changed files listed in 'files' + and having listed parents. 'commit' is a commit object + containing at a minimum the author, date, and message for this + changeset. 'files' is a list of (path, version) tuples, + 'copies' is a dictionary mapping destinations to sources, + 'source' is the source repository, and 'revmap' is a mapfile + of source revisions to converted revisions. Only getfile(), + getmode(), and lookuprev() should be called on 'source'. + + Note that the sink repository is not told to update itself to + a particular revision (or even what that revision would be) + before it receives the file data. + """ + raise NotImplementedError() + + def puttags(self, tags): + """Put tags into sink. + + tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string. + Return a pair (tag_revision, tag_parent_revision), or (None, None) + if nothing was changed. + """ + raise NotImplementedError() + + def setbranch(self, branch, pbranches): + """Set the current branch name. Called before the first putcommit + on the branch. + branch: branch name for subsequent commits + pbranches: (converted parent revision, parent branch) tuples""" + pass + + def setfilemapmode(self, active): + """Tell the destination that we're using a filemap + + Some converter_sources (svn in particular) can claim that a file + was changed in a revision, even if there was no change. This method + tells the destination that we're using a filemap and that it should + filter empty revisions. + """ + pass + + def before(self): + pass + + def after(self): + pass + + +class commandline(object): + def __init__(self, ui, command): + self.ui = ui + self.command = command + + def prerun(self): + pass + + def postrun(self): + pass + + def _cmdline(self, cmd, *args, **kwargs): + cmdline = [self.command, cmd] + list(args) + for k, v in kwargs.iteritems(): + if len(k) == 1: + cmdline.append('-' + k) + else: + cmdline.append('--' + k.replace('_', '-')) + try: + if len(k) == 1: + cmdline.append('' + v) + else: + cmdline[-1] += '=' + v + except TypeError: + pass + cmdline = [util.shellquote(arg) for arg in cmdline] + if not self.ui.debugflag: + cmdline += ['2>', util.nulldev] + cmdline += ['<', util.nulldev] + cmdline = ' '.join(cmdline) + return cmdline + + def _run(self, cmd, *args, **kwargs): + cmdline = self._cmdline(cmd, *args, **kwargs) + self.ui.debug('running: %s\n' % (cmdline,)) + self.prerun() + try: + return util.popen(cmdline) + finally: + self.postrun() + + def run(self, cmd, *args, **kwargs): + fp = self._run(cmd, *args, **kwargs) + output = fp.read() + self.ui.debug(output) + return output, fp.close() + + def runlines(self, cmd, *args, **kwargs): + fp = self._run(cmd, *args, **kwargs) + output = fp.readlines() + self.ui.debug(''.join(output)) + return output, fp.close() + + def checkexit(self, status, output=''): + if status: + if output: + self.ui.warn(_('%s error:\n') % self.command) + self.ui.warn(output) + msg = util.explain_exit(status)[0] + raise util.Abort('%s %s' % (self.command, msg)) + + def run0(self, cmd, *args, **kwargs): + output, status = self.run(cmd, *args, **kwargs) + self.checkexit(status, output) + return output + + def runlines0(self, cmd, *args, **kwargs): + output, status = self.runlines(cmd, *args, **kwargs) + self.checkexit(status, ''.join(output)) + return output + + def getargmax(self): + if '_argmax' in self.__dict__: + return self._argmax + + # POSIX requires at least 4096 bytes for ARG_MAX + self._argmax = 4096 + try: + self._argmax = os.sysconf("SC_ARG_MAX") + except: + pass + + # Windows shells impose their own limits on command line length, + # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes + # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for + # details about cmd.exe limitations. + + # Since ARG_MAX is for command line _and_ environment, lower our limit + # (and make happy Windows shells while doing this). + + self._argmax = self._argmax / 2 - 1 + return self._argmax + + def limit_arglist(self, arglist, cmd, *args, **kwargs): + limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs)) + bytes = 0 + fl = [] + for fn in arglist: + b = len(fn) + 3 + if bytes + b < limit or len(fl) == 0: + fl.append(fn) + bytes += b + else: + yield fl + fl = [fn] + bytes = b + if fl: + yield fl + + def xargs(self, arglist, cmd, *args, **kwargs): + for l in self.limit_arglist(arglist, cmd, *args, **kwargs): + self.run0(cmd, *(list(args) + l), **kwargs) + +class mapfile(dict): + def __init__(self, ui, path): + super(mapfile, self).__init__() + self.ui = ui + self.path = path + self.fp = None + self.order = [] + self._read() + + def _read(self): + if not self.path: + return + try: + fp = open(self.path, 'r') + except IOError, err: + if err.errno != errno.ENOENT: + raise + return + for i, line in enumerate(fp): + try: + key, value = line.splitlines()[0].rsplit(' ', 1) + except ValueError: + raise util.Abort( + _('syntax error in %s(%d): key/value pair expected') + % (self.path, i + 1)) + if key not in self: + self.order.append(key) + super(mapfile, self).__setitem__(key, value) + fp.close() + + def __setitem__(self, key, value): + if self.fp is None: + try: + self.fp = open(self.path, 'a') + except IOError, err: + raise util.Abort(_('could not open map file %r: %s') % + (self.path, err.strerror)) + self.fp.write('%s %s\n' % (key, value)) + self.fp.flush() + super(mapfile, self).__setitem__(key, value) + + def close(self): + if self.fp: + self.fp.close() + self.fp = None diff --git a/plugins/hg4idea/testData/bin/hgext/convert/convcmd.py b/plugins/hg4idea/testData/bin/hgext/convert/convcmd.py new file mode 100644 index 000000000000..0c0eb52011f4 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/convcmd.py @@ -0,0 +1,404 @@ +# convcmd - convert extension commands definition +# +# Copyright 2005-2007 Matt Mackall +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from common import NoRepo, MissingTool, SKIPREV, mapfile +from cvs import convert_cvs +from darcs import darcs_source +from git import convert_git +from hg import mercurial_source, mercurial_sink +from subversion import svn_source, svn_sink +from monotone import monotone_source +from gnuarch import gnuarch_source +from bzr import bzr_source +from p4 import p4_source +import filemap + +import os, shutil +from mercurial import hg, util, encoding +from mercurial.i18n import _ + +orig_encoding = 'ascii' + +def recode(s): + if isinstance(s, unicode): + return s.encode(orig_encoding, 'replace') + else: + return s.decode('utf-8').encode(orig_encoding, 'replace') + +source_converters = [ + ('cvs', convert_cvs, 'branchsort'), + ('git', convert_git, 'branchsort'), + ('svn', svn_source, 'branchsort'), + ('hg', mercurial_source, 'sourcesort'), + ('darcs', darcs_source, 'branchsort'), + ('mtn', monotone_source, 'branchsort'), + ('gnuarch', gnuarch_source, 'branchsort'), + ('bzr', bzr_source, 'branchsort'), + ('p4', p4_source, 'branchsort'), + ] + +sink_converters = [ + ('hg', mercurial_sink), + ('svn', svn_sink), + ] + +def convertsource(ui, path, type, rev): + exceptions = [] + if type and type not in [s[0] for s in source_converters]: + raise util.Abort(_('%s: invalid source repository type') % type) + for name, source, sortmode in source_converters: + try: + if not type or name == type: + return source(ui, path, rev), sortmode + except (NoRepo, MissingTool), inst: + exceptions.append(inst) + if not ui.quiet: + for inst in exceptions: + ui.write("%s\n" % inst) + raise util.Abort(_('%s: missing or unsupported repository') % path) + +def convertsink(ui, path, type): + if type and type not in [s[0] for s in sink_converters]: + raise util.Abort(_('%s: invalid destination repository type') % type) + for name, sink in sink_converters: + try: + if not type or name == type: + return sink(ui, path) + except NoRepo, inst: + ui.note(_("convert: %s\n") % inst) + raise util.Abort(_('%s: unknown repository type') % path) + +class converter(object): + def __init__(self, ui, source, dest, revmapfile, opts): + + self.source = source + self.dest = dest + self.ui = ui + self.opts = opts + self.commitcache = {} + self.authors = {} + self.authorfile = None + + # Record converted revisions persistently: maps source revision + # ID to target revision ID (both strings). (This is how + # incremental conversions work.) + self.map = mapfile(ui, revmapfile) + + # Read first the dst author map if any + authorfile = self.dest.authorfile() + if authorfile and os.path.exists(authorfile): + self.readauthormap(authorfile) + # Extend/Override with new author map if necessary + if opts.get('authors'): + self.readauthormap(opts.get('authors')) + self.authorfile = self.dest.authorfile() + + self.splicemap = mapfile(ui, opts.get('splicemap')) + self.branchmap = mapfile(ui, opts.get('branchmap')) + + def walktree(self, heads): + '''Return a mapping that identifies the uncommitted parents of every + uncommitted changeset.''' + visit = heads + known = set() + parents = {} + while visit: + n = visit.pop(0) + if n in known or n in self.map: + continue + known.add(n) + commit = self.cachecommit(n) + parents[n] = [] + for p in commit.parents: + parents[n].append(p) + visit.append(p) + + return parents + + def toposort(self, parents, sortmode): + '''Return an ordering such that every uncommitted changeset is + preceeded by all its uncommitted ancestors.''' + + def mapchildren(parents): + """Return a (children, roots) tuple where 'children' maps parent + revision identifiers to children ones, and 'roots' is the list of + revisions without parents. 'parents' must be a mapping of revision + identifier to its parents ones. + """ + visit = parents.keys() + seen = set() + children = {} + roots = [] + + while visit: + n = visit.pop(0) + if n in seen: + continue + seen.add(n) + # Ensure that nodes without parents are present in the + # 'children' mapping. + children.setdefault(n, []) + hasparent = False + for p in parents[n]: + if not p in self.map: + visit.append(p) + hasparent = True + children.setdefault(p, []).append(n) + if not hasparent: + roots.append(n) + + return children, roots + + # Sort functions are supposed to take a list of revisions which + # can be converted immediately and pick one + + def makebranchsorter(): + """If the previously converted revision has a child in the + eligible revisions list, pick it. Return the list head + otherwise. Branch sort attempts to minimize branch + switching, which is harmful for Mercurial backend + compression. + """ + prev = [None] + def picknext(nodes): + next = nodes[0] + for n in nodes: + if prev[0] in parents[n]: + next = n + break + prev[0] = next + return next + return picknext + + def makesourcesorter(): + """Source specific sort.""" + keyfn = lambda n: self.commitcache[n].sortkey + def picknext(nodes): + return sorted(nodes, key=keyfn)[0] + return picknext + + def makedatesorter(): + """Sort revisions by date.""" + dates = {} + def getdate(n): + if n not in dates: + dates[n] = util.parsedate(self.commitcache[n].date) + return dates[n] + + def picknext(nodes): + return min([(getdate(n), n) for n in nodes])[1] + + return picknext + + if sortmode == 'branchsort': + picknext = makebranchsorter() + elif sortmode == 'datesort': + picknext = makedatesorter() + elif sortmode == 'sourcesort': + picknext = makesourcesorter() + else: + raise util.Abort(_('unknown sort mode: %s') % sortmode) + + children, actives = mapchildren(parents) + + s = [] + pendings = {} + while actives: + n = picknext(actives) + actives.remove(n) + s.append(n) + + # Update dependents list + for c in children.get(n, []): + if c not in pendings: + pendings[c] = [p for p in parents[c] if p not in self.map] + try: + pendings[c].remove(n) + except ValueError: + raise util.Abort(_('cycle detected between %s and %s') + % (recode(c), recode(n))) + if not pendings[c]: + # Parents are converted, node is eligible + actives.insert(0, c) + pendings[c] = None + + if len(s) != len(parents): + raise util.Abort(_("not all revisions were sorted")) + + return s + + def writeauthormap(self): + authorfile = self.authorfile + if authorfile: + self.ui.status(_('Writing author map file %s\n') % authorfile) + ofile = open(authorfile, 'w+') + for author in self.authors: + ofile.write("%s=%s\n" % (author, self.authors[author])) + ofile.close() + + def readauthormap(self, authorfile): + afile = open(authorfile, 'r') + for line in afile: + + line = line.strip() + if not line or line.startswith('#'): + continue + + try: + srcauthor, dstauthor = line.split('=', 1) + except ValueError: + msg = _('Ignoring bad line in author map file %s: %s\n') + self.ui.warn(msg % (authorfile, line.rstrip())) + continue + + srcauthor = srcauthor.strip() + dstauthor = dstauthor.strip() + if self.authors.get(srcauthor) in (None, dstauthor): + msg = _('mapping author %s to %s\n') + self.ui.debug(msg % (srcauthor, dstauthor)) + self.authors[srcauthor] = dstauthor + continue + + m = _('overriding mapping for author %s, was %s, will be %s\n') + self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor)) + + afile.close() + + def cachecommit(self, rev): + commit = self.source.getcommit(rev) + commit.author = self.authors.get(commit.author, commit.author) + commit.branch = self.branchmap.get(commit.branch, commit.branch) + self.commitcache[rev] = commit + return commit + + def copy(self, rev): + commit = self.commitcache[rev] + + changes = self.source.getchanges(rev) + if isinstance(changes, basestring): + if changes == SKIPREV: + dest = SKIPREV + else: + dest = self.map[changes] + self.map[rev] = dest + return + files, copies = changes + pbranches = [] + if commit.parents: + for prev in commit.parents: + if prev not in self.commitcache: + self.cachecommit(prev) + pbranches.append((self.map[prev], + self.commitcache[prev].branch)) + self.dest.setbranch(commit.branch, pbranches) + try: + parents = self.splicemap[rev].replace(',', ' ').split() + self.ui.status(_('spliced in %s as parents of %s\n') % + (parents, rev)) + parents = [self.map.get(p, p) for p in parents] + except KeyError: + parents = [b[0] for b in pbranches] + newnode = self.dest.putcommit(files, copies, parents, commit, + self.source, self.map) + self.source.converted(rev, newnode) + self.map[rev] = newnode + + def convert(self, sortmode): + try: + self.source.before() + self.dest.before() + self.source.setrevmap(self.map) + self.ui.status(_("scanning source...\n")) + heads = self.source.getheads() + parents = self.walktree(heads) + self.ui.status(_("sorting...\n")) + t = self.toposort(parents, sortmode) + num = len(t) + c = None + + self.ui.status(_("converting...\n")) + for c in t: + num -= 1 + desc = self.commitcache[c].desc + if "\n" in desc: + desc = desc.splitlines()[0] + # convert log message to local encoding without using + # tolocal() because encoding.encoding conver() use it as + # 'utf-8' + self.ui.status("%d %s\n" % (num, recode(desc))) + self.ui.note(_("source: %s\n") % recode(c)) + self.copy(c) + + tags = self.source.gettags() + ctags = {} + for k in tags: + v = tags[k] + if self.map.get(v, SKIPREV) != SKIPREV: + ctags[k] = self.map[v] + + if c and ctags: + nrev, tagsparent = self.dest.puttags(ctags) + if nrev and tagsparent: + # write another hash correspondence to override the previous + # one so we don't end up with extra tag heads + tagsparents = [e for e in self.map.iteritems() + if e[1] == tagsparent] + if tagsparents: + self.map[tagsparents[0][0]] = nrev + + self.writeauthormap() + finally: + self.cleanup() + + def cleanup(self): + try: + self.dest.after() + finally: + self.source.after() + self.map.close() + +def convert(ui, src, dest=None, revmapfile=None, **opts): + global orig_encoding + orig_encoding = encoding.encoding + encoding.encoding = 'UTF-8' + + if not dest: + dest = hg.defaultdest(src) + "-hg" + ui.status(_("assuming destination %s\n") % dest) + + destc = convertsink(ui, dest, opts.get('dest_type')) + + try: + srcc, defaultsort = convertsource(ui, src, opts.get('source_type'), + opts.get('rev')) + except Exception: + for path in destc.created: + shutil.rmtree(path, True) + raise + + sortmodes = ('branchsort', 'datesort', 'sourcesort') + sortmode = [m for m in sortmodes if opts.get(m)] + if len(sortmode) > 1: + raise util.Abort(_('more than one sort mode specified')) + sortmode = sortmode and sortmode[0] or defaultsort + if sortmode == 'sourcesort' and not srcc.hasnativeorder(): + raise util.Abort(_('--sourcesort is not supported by this data source')) + + fmap = opts.get('filemap') + if fmap: + srcc = filemap.filemap_source(ui, srcc, fmap) + destc.setfilemapmode(True) + + if not revmapfile: + try: + revmapfile = destc.revmapfile() + except: + revmapfile = os.path.join(destc, "map") + + c = converter(ui, srcc, destc, revmapfile, opts) + c.convert(sortmode) + diff --git a/plugins/hg4idea/testData/bin/hgext/convert/cvs.py b/plugins/hg4idea/testData/bin/hgext/convert/cvs.py new file mode 100644 index 000000000000..fc40b4b39200 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/cvs.py @@ -0,0 +1,282 @@ +# cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport +# +# Copyright 2005-2009 Matt Mackall and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import os, locale, re, socket, errno +from cStringIO import StringIO +from mercurial import util +from mercurial.i18n import _ + +from common import NoRepo, commit, converter_source, checktool +import cvsps + +class convert_cvs(converter_source): + def __init__(self, ui, path, rev=None): + super(convert_cvs, self).__init__(ui, path, rev=rev) + + cvs = os.path.join(path, "CVS") + if not os.path.exists(cvs): + raise NoRepo(_("%s does not look like a CVS checkout") % path) + + checktool('cvs') + + self.changeset = None + self.files = {} + self.tags = {} + self.lastbranch = {} + self.socket = None + self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1] + self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1] + self.encoding = locale.getpreferredencoding() + + self._connect() + + def _parse(self): + if self.changeset is not None: + return + self.changeset = {} + + maxrev = 0 + if self.rev: + # TODO: handle tags + try: + # patchset number? + maxrev = int(self.rev) + except ValueError: + raise util.Abort(_('revision %s is not a patchset number') + % self.rev) + + d = os.getcwd() + try: + os.chdir(self.path) + id = None + state = 0 + filerevids = {} + + cache = 'update' + if not self.ui.configbool('convert', 'cvsps.cache', True): + cache = None + db = cvsps.createlog(self.ui, cache=cache) + db = cvsps.createchangeset(self.ui, db, + fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)), + mergeto=self.ui.config('convert', 'cvsps.mergeto', None), + mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None)) + + for cs in db: + if maxrev and cs.id > maxrev: + break + id = str(cs.id) + cs.author = self.recode(cs.author) + self.lastbranch[cs.branch] = id + cs.comment = self.recode(cs.comment) + date = util.datestr(cs.date) + self.tags.update(dict.fromkeys(cs.tags, id)) + + files = {} + for f in cs.entries: + files[f.file] = "%s%s" % ('.'.join([str(x) + for x in f.revision]), + ['', '(DEAD)'][f.dead]) + + # add current commit to set + c = commit(author=cs.author, date=date, + parents=[str(p.id) for p in cs.parents], + desc=cs.comment, branch=cs.branch or '') + self.changeset[id] = c + self.files[id] = files + + self.heads = self.lastbranch.values() + finally: + os.chdir(d) + + def _connect(self): + root = self.cvsroot + conntype = None + user, host = None, None + cmd = ['cvs', 'server'] + + self.ui.status(_("connecting to %s\n") % root) + + if root.startswith(":pserver:"): + root = root[9:] + m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', + root) + if m: + conntype = "pserver" + user, passw, serv, port, root = m.groups() + if not user: + user = "anonymous" + if not port: + port = 2401 + else: + port = int(port) + format0 = ":pserver:%s@%s:%s" % (user, serv, root) + format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root) + + if not passw: + passw = "A" + cvspass = os.path.expanduser("~/.cvspass") + try: + pf = open(cvspass) + for line in pf.read().splitlines(): + part1, part2 = line.split(' ', 1) + if part1 == '/1': + # /1 :pserver:user@example.com:2401/cvsroot/foo Ah 0: + data = fp.read(min(count, chunksize)) + if not data: + raise util.Abort(_("%d bytes missing from remote file") + % count) + count -= len(data) + output.write(data) + return output.getvalue() + + if rev.endswith("(DEAD)"): + raise IOError + + args = ("-N -P -kk -r %s --" % rev).split() + args.append(self.cvsrepo + '/' + name) + for x in args: + self.writep.write("Argument %s\n" % x) + self.writep.write("Directory .\n%s\nco\n" % self.realroot) + self.writep.flush() + + data = "" + mode = None + while 1: + line = self.readp.readline() + if line.startswith("Created ") or line.startswith("Updated "): + self.readp.readline() # path + self.readp.readline() # entries + mode = self.readp.readline()[:-1] + count = int(self.readp.readline()[:-1]) + data = chunkedread(self.readp, count) + elif line.startswith(" "): + data += line[1:] + elif line.startswith("M "): + pass + elif line.startswith("Mbinary "): + count = int(self.readp.readline()[:-1]) + data = chunkedread(self.readp, count) + else: + if line == "ok\n": + if mode is None: + raise util.Abort(_('malformed response from CVS')) + return (data, "x" in mode and "x" or "") + elif line.startswith("E "): + self.ui.warn(_("cvs server: %s\n") % line[2:]) + elif line.startswith("Remove"): + self.readp.readline() + else: + raise util.Abort(_("unknown CVS response: %s") % line) + + def getfile(self, file, rev): + self._parse() + data, mode = self._getfile(file, rev) + self.modecache[(file, rev)] = mode + return data + + def getmode(self, file, rev): + return self.modecache[(file, rev)] + + def getchanges(self, rev): + self._parse() + self.modecache = {} + return sorted(self.files[rev].iteritems()), {} + + def getcommit(self, rev): + self._parse() + return self.changeset[rev] + + def gettags(self): + self._parse() + return self.tags + + def getchangedfiles(self, rev, i): + self._parse() + return sorted(self.files[rev]) diff --git a/plugins/hg4idea/testData/bin/hgext/convert/cvsps.py b/plugins/hg4idea/testData/bin/hgext/convert/cvsps.py new file mode 100644 index 000000000000..38dbcff03c19 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/cvsps.py @@ -0,0 +1,847 @@ +# Mercurial built-in replacement for cvsps. +# +# Copyright 2008, Frank Kingswood +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import os +import re +import cPickle as pickle +from mercurial import util +from mercurial.i18n import _ +from mercurial import hook + +class logentry(object): + '''Class logentry has the following attributes: + .author - author name as CVS knows it + .branch - name of branch this revision is on + .branches - revision tuple of branches starting at this revision + .comment - commit message + .date - the commit date as a (time, tz) tuple + .dead - true if file revision is dead + .file - Name of file + .lines - a tuple (+lines, -lines) or None + .parent - Previous revision of this entry + .rcs - name of file as returned from CVS + .revision - revision number as tuple + .tags - list of tags on the file + .synthetic - is this a synthetic "file ... added on ..." revision? + .mergepoint- the branch that has been merged from + (if present in rlog output) + .branchpoints- the branches that start at the current entry + ''' + def __init__(self, **entries): + self.synthetic = False + self.__dict__.update(entries) + + def __repr__(self): + return "<%s at 0x%x: %s %s>" % (self.__class__.__name__, + id(self), + self.file, + ".".join(map(str, self.revision))) + +class logerror(Exception): + pass + +def getrepopath(cvspath): + """Return the repository path from a CVS path. + + >>> getrepopath('/foo/bar') + '/foo/bar' + >>> getrepopath('c:/foo/bar') + 'c:/foo/bar' + >>> getrepopath(':pserver:10/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:10c:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:c:/foo/bar') + 'c:/foo/bar' + >>> getrepopath(':pserver:truc@foo.bar:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar') + 'c:/foo/bar' + """ + # According to CVS manual, CVS paths are expressed like: + # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository + # + # Unfortunately, Windows absolute paths start with a drive letter + # like 'c:' making it harder to parse. Here we assume that drive + # letters are only one character long and any CVS component before + # the repository path is at least 2 characters long, and use this + # to disambiguate. + parts = cvspath.split(':') + if len(parts) == 1: + return parts[0] + # Here there is an ambiguous case if we have a port number + # immediately followed by a Windows driver letter. We assume this + # never happens and decide it must be CVS path component, + # therefore ignoring it. + if len(parts[-2]) > 1: + return parts[-1].lstrip('0123456789') + return parts[-2] + ':' + parts[-1] + +def createlog(ui, directory=None, root="", rlog=True, cache=None): + '''Collect the CVS rlog''' + + # Because we store many duplicate commit log messages, reusing strings + # saves a lot of memory and pickle storage space. + _scache = {} + def scache(s): + "return a shared version of a string" + return _scache.setdefault(s, s) + + ui.status(_('collecting CVS rlog\n')) + + log = [] # list of logentry objects containing the CVS state + + # patterns to match in CVS (r)log output, by state of use + re_00 = re.compile('RCS file: (.+)$') + re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$') + re_02 = re.compile('cvs (r?log|server): (.+)\n$') + re_03 = re.compile("(Cannot access.+CVSROOT)|" + "(can't create temporary directory.+)$") + re_10 = re.compile('Working file: (.+)$') + re_20 = re.compile('symbolic names:') + re_30 = re.compile('\t(.+): ([\\d.]+)$') + re_31 = re.compile('----------------------------$') + re_32 = re.compile('=======================================' + '======================================$') + re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$') + re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);' + r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?' + r'(.*mergepoint:\s+([^;]+);)?') + re_70 = re.compile('branches: (.+);$') + + file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch') + + prefix = '' # leading path to strip of what we get from CVS + + if directory is None: + # Current working directory + + # Get the real directory in the repository + try: + prefix = open(os.path.join('CVS','Repository')).read().strip() + directory = prefix + if prefix == ".": + prefix = "" + except IOError: + raise logerror(_('not a CVS sandbox')) + + if prefix and not prefix.endswith(os.sep): + prefix += os.sep + + # Use the Root file in the sandbox, if it exists + try: + root = open(os.path.join('CVS','Root')).read().strip() + except IOError: + pass + + if not root: + root = os.environ.get('CVSROOT', '') + + # read log cache if one exists + oldlog = [] + date = None + + if cache: + cachedir = os.path.expanduser('~/.hg.cvsps') + if not os.path.exists(cachedir): + os.mkdir(cachedir) + + # The cvsps cache pickle needs a uniquified name, based on the + # repository location. The address may have all sort of nasties + # in it, slashes, colons and such. So here we take just the + # alphanumerics, concatenated in a way that does not mix up the + # various components, so that + # :pserver:user@server:/path + # and + # /pserver/user/server/path + # are mapped to different cache file names. + cachefile = root.split(":") + [directory, "cache"] + cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s] + cachefile = os.path.join(cachedir, + '.'.join([s for s in cachefile if s])) + + if cache == 'update': + try: + ui.note(_('reading cvs log cache %s\n') % cachefile) + oldlog = pickle.load(open(cachefile)) + ui.note(_('cache has %d log entries\n') % len(oldlog)) + except Exception, e: + ui.note(_('error reading cache: %r\n') % e) + + if oldlog: + date = oldlog[-1].date # last commit date as a (time,tz) tuple + date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2') + + # build the CVS commandline + cmd = ['cvs', '-q'] + if root: + cmd.append('-d%s' % root) + p = util.normpath(getrepopath(root)) + if not p.endswith('/'): + p += '/' + if prefix: + # looks like normpath replaces "" by "." + prefix = p + util.normpath(prefix) + else: + prefix = p + cmd.append(['log', 'rlog'][rlog]) + if date: + # no space between option and date string + cmd.append('-d>%s' % date) + cmd.append(directory) + + # state machine begins here + tags = {} # dictionary of revisions on current file with their tags + branchmap = {} # mapping between branch names and revision numbers + state = 0 + store = False # set when a new record can be appended + + cmd = [util.shellquote(arg) for arg in cmd] + ui.note(_("running %s\n") % (' '.join(cmd))) + ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root)) + + pfp = util.popen(' '.join(cmd)) + peek = pfp.readline() + while True: + line = peek + if line == '': + break + peek = pfp.readline() + if line.endswith('\n'): + line = line[:-1] + #ui.debug('state=%d line=%r\n' % (state, line)) + + if state == 0: + # initial state, consume input until we see 'RCS file' + match = re_00.match(line) + if match: + rcs = match.group(1) + tags = {} + if rlog: + filename = util.normpath(rcs[:-2]) + if filename.startswith(prefix): + filename = filename[len(prefix):] + if filename.startswith('/'): + filename = filename[1:] + if filename.startswith('Attic/'): + filename = filename[6:] + else: + filename = filename.replace('/Attic/', '/') + state = 2 + continue + state = 1 + continue + match = re_01.match(line) + if match: + raise Exception(match.group(1)) + match = re_02.match(line) + if match: + raise Exception(match.group(2)) + if re_03.match(line): + raise Exception(line) + + elif state == 1: + # expect 'Working file' (only when using log instead of rlog) + match = re_10.match(line) + assert match, _('RCS file must be followed by working file') + filename = util.normpath(match.group(1)) + state = 2 + + elif state == 2: + # expect 'symbolic names' + if re_20.match(line): + branchmap = {} + state = 3 + + elif state == 3: + # read the symbolic names and store as tags + match = re_30.match(line) + if match: + rev = [int(x) for x in match.group(2).split('.')] + + # Convert magic branch number to an odd-numbered one + revn = len(rev) + if revn > 3 and (revn % 2) == 0 and rev[-2] == 0: + rev = rev[:-2] + rev[-1:] + rev = tuple(rev) + + if rev not in tags: + tags[rev] = [] + tags[rev].append(match.group(1)) + branchmap[match.group(1)] = match.group(2) + + elif re_31.match(line): + state = 5 + elif re_32.match(line): + state = 0 + + elif state == 4: + # expecting '------' separator before first revision + if re_31.match(line): + state = 5 + else: + assert not re_32.match(line), _('must have at least ' + 'some revisions') + + elif state == 5: + # expecting revision number and possibly (ignored) lock indication + # we create the logentry here from values stored in states 0 to 4, + # as this state is re-entered for subsequent revisions of a file. + match = re_50.match(line) + assert match, _('expected revision number') + e = logentry(rcs=scache(rcs), file=scache(filename), + revision=tuple([int(x) for x in match.group(1).split('.')]), + branches=[], parent=None) + state = 6 + + elif state == 6: + # expecting date, author, state, lines changed + match = re_60.match(line) + assert match, _('revision must be followed by date line') + d = match.group(1) + if d[2] == '/': + # Y2K + d = '19' + d + + if len(d.split()) != 3: + # cvs log dates always in GMT + d = d + ' UTC' + e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', + '%Y/%m/%d %H:%M:%S', + '%Y-%m-%d %H:%M:%S']) + e.author = scache(match.group(2)) + e.dead = match.group(3).lower() == 'dead' + + if match.group(5): + if match.group(6): + e.lines = (int(match.group(5)), int(match.group(6))) + else: + e.lines = (int(match.group(5)), 0) + elif match.group(6): + e.lines = (0, int(match.group(6))) + else: + e.lines = None + + if match.group(7): # cvsnt mergepoint + myrev = match.group(8).split('.') + if len(myrev) == 2: # head + e.mergepoint = 'HEAD' + else: + myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]]) + branches = [b for b in branchmap if branchmap[b] == myrev] + assert len(branches) == 1, 'unknown branch: %s' % e.mergepoint + e.mergepoint = branches[0] + else: + e.mergepoint = None + e.comment = [] + state = 7 + + elif state == 7: + # read the revision numbers of branches that start at this revision + # or store the commit log message otherwise + m = re_70.match(line) + if m: + e.branches = [tuple([int(y) for y in x.strip().split('.')]) + for x in m.group(1).split(';')] + state = 8 + elif re_31.match(line) and re_50.match(peek): + state = 5 + store = True + elif re_32.match(line): + state = 0 + store = True + else: + e.comment.append(line) + + elif state == 8: + # store commit log message + if re_31.match(line): + state = 5 + store = True + elif re_32.match(line): + state = 0 + store = True + else: + e.comment.append(line) + + # When a file is added on a branch B1, CVS creates a synthetic + # dead trunk revision 1.1 so that the branch has a root. + # Likewise, if you merge such a file to a later branch B2 (one + # that already existed when the file was added on B1), CVS + # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop + # these revisions now, but mark them synthetic so + # createchangeset() can take care of them. + if (store and + e.dead and + e.revision[-1] == 1 and # 1.1 or 1.1.x.1 + len(e.comment) == 1 and + file_added_re.match(e.comment[0])): + ui.debug('found synthetic revision in %s: %r\n' + % (e.rcs, e.comment[0])) + e.synthetic = True + + if store: + # clean up the results and save in the log. + store = False + e.tags = sorted([scache(x) for x in tags.get(e.revision, [])]) + e.comment = scache('\n'.join(e.comment)) + + revn = len(e.revision) + if revn > 3 and (revn % 2) == 0: + e.branch = tags.get(e.revision[:-1], [None])[0] + else: + e.branch = None + + # find the branches starting from this revision + branchpoints = set() + for branch, revision in branchmap.iteritems(): + revparts = tuple([int(i) for i in revision.split('.')]) + if len(revparts) < 2: # bad tags + continue + if revparts[-2] == 0 and revparts[-1] % 2 == 0: + # normal branch + if revparts[:-2] == e.revision: + branchpoints.add(branch) + elif revparts == (1, 1, 1): # vendor branch + if revparts in e.branches: + branchpoints.add(branch) + e.branchpoints = branchpoints + + log.append(e) + + if len(log) % 100 == 0: + ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n') + + log.sort(key=lambda x: (x.rcs, x.revision)) + + # find parent revisions of individual files + versions = {} + for e in log: + branch = e.revision[:-1] + p = versions.get((e.rcs, branch), None) + if p is None: + p = e.revision[:-2] + e.parent = p + versions[(e.rcs, branch)] = e.revision + + # update the log cache + if cache: + if log: + # join up the old and new logs + log.sort(key=lambda x: x.date) + + if oldlog and oldlog[-1].date >= log[0].date: + raise logerror(_('log cache overlaps with new log entries,' + ' re-run without cache.')) + + log = oldlog + log + + # write the new cachefile + ui.note(_('writing cvs log cache %s\n') % cachefile) + pickle.dump(log, open(cachefile, 'w')) + else: + log = oldlog + + ui.status(_('%d log entries\n') % len(log)) + + hook.hook(ui, None, "cvslog", True, log=log) + + return log + + +class changeset(object): + '''Class changeset has the following attributes: + .id - integer identifying this changeset (list index) + .author - author name as CVS knows it + .branch - name of branch this changeset is on, or None + .comment - commit message + .date - the commit date as a (time,tz) tuple + .entries - list of logentry objects in this changeset + .parents - list of one or two parent changesets + .tags - list of tags on this changeset + .synthetic - from synthetic revision "file ... added on branch ..." + .mergepoint- the branch that has been merged from + (if present in rlog output) + .branchpoints- the branches that start at the current entry + ''' + def __init__(self, **entries): + self.synthetic = False + self.__dict__.update(entries) + + def __repr__(self): + return "<%s at 0x%x: %s>" % (self.__class__.__name__, + id(self), + getattr(self, 'id', "(no id)")) + +def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None): + '''Convert log into changesets.''' + + ui.status(_('creating changesets\n')) + + # Merge changesets + + log.sort(key=lambda x: (x.comment, x.author, x.branch, x.date)) + + changesets = [] + files = set() + c = None + for i, e in enumerate(log): + + # Check if log entry belongs to the current changeset or not. + + # Since CVS is file centric, two different file revisions with + # different branchpoints should be treated as belonging to two + # different changesets (and the ordering is important and not + # honoured by cvsps at this point). + # + # Consider the following case: + # foo 1.1 branchpoints: [MYBRANCH] + # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2] + # + # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a + # later version of foo may be in MYBRANCH2, so foo should be the + # first changeset and bar the next and MYBRANCH and MYBRANCH2 + # should both start off of the bar changeset. No provisions are + # made to ensure that this is, in fact, what happens. + if not (c and + e.comment == c.comment and + e.author == c.author and + e.branch == c.branch and + (not hasattr(e, 'branchpoints') or + not hasattr (c, 'branchpoints') or + e.branchpoints == c.branchpoints) and + ((c.date[0] + c.date[1]) <= + (e.date[0] + e.date[1]) <= + (c.date[0] + c.date[1]) + fuzz) and + e.file not in files): + c = changeset(comment=e.comment, author=e.author, + branch=e.branch, date=e.date, entries=[], + mergepoint=getattr(e, 'mergepoint', None), + branchpoints=getattr(e, 'branchpoints', set())) + changesets.append(c) + files = set() + if len(changesets) % 100 == 0: + t = '%d %s' % (len(changesets), repr(e.comment)[1:-1]) + ui.status(util.ellipsis(t, 80) + '\n') + + c.entries.append(e) + files.add(e.file) + c.date = e.date # changeset date is date of latest commit in it + + # Mark synthetic changesets + + for c in changesets: + # Synthetic revisions always get their own changeset, because + # the log message includes the filename. E.g. if you add file3 + # and file4 on a branch, you get four log entries and three + # changesets: + # "File file3 was added on branch ..." (synthetic, 1 entry) + # "File file4 was added on branch ..." (synthetic, 1 entry) + # "Add file3 and file4 to fix ..." (real, 2 entries) + # Hence the check for 1 entry here. + c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic + + # Sort files in each changeset + + for c in changesets: + def pathcompare(l, r): + 'Mimic cvsps sorting order' + l = l.split('/') + r = r.split('/') + nl = len(l) + nr = len(r) + n = min(nl, nr) + for i in range(n): + if i + 1 == nl and nl < nr: + return -1 + elif i + 1 == nr and nl > nr: + return +1 + elif l[i] < r[i]: + return -1 + elif l[i] > r[i]: + return +1 + return 0 + def entitycompare(l, r): + return pathcompare(l.file, r.file) + + c.entries.sort(entitycompare) + + # Sort changesets by date + + def cscmp(l, r): + d = sum(l.date) - sum(r.date) + if d: + return d + + # detect vendor branches and initial commits on a branch + le = {} + for e in l.entries: + le[e.rcs] = e.revision + re = {} + for e in r.entries: + re[e.rcs] = e.revision + + d = 0 + for e in l.entries: + if re.get(e.rcs, None) == e.parent: + assert not d + d = 1 + break + + for e in r.entries: + if le.get(e.rcs, None) == e.parent: + assert not d + d = -1 + break + + return d + + changesets.sort(cscmp) + + # Collect tags + + globaltags = {} + for c in changesets: + for e in c.entries: + for tag in e.tags: + # remember which is the latest changeset to have this tag + globaltags[tag] = c + + for c in changesets: + tags = set() + for e in c.entries: + tags.update(e.tags) + # remember tags only if this is the latest changeset to have it + c.tags = sorted(tag for tag in tags if globaltags[tag] is c) + + # Find parent changesets, handle {{mergetobranch BRANCHNAME}} + # by inserting dummy changesets with two parents, and handle + # {{mergefrombranch BRANCHNAME}} by setting two parents. + + if mergeto is None: + mergeto = r'{{mergetobranch ([-\w]+)}}' + if mergeto: + mergeto = re.compile(mergeto) + + if mergefrom is None: + mergefrom = r'{{mergefrombranch ([-\w]+)}}' + if mergefrom: + mergefrom = re.compile(mergefrom) + + versions = {} # changeset index where we saw any particular file version + branches = {} # changeset index where we saw a branch + n = len(changesets) + i = 0 + while i < n: + c = changesets[i] + + for f in c.entries: + versions[(f.rcs, f.revision)] = i + + p = None + if c.branch in branches: + p = branches[c.branch] + else: + # first changeset on a new branch + # the parent is a changeset with the branch in its + # branchpoints such that it is the latest possible + # commit without any intervening, unrelated commits. + + for candidate in xrange(i): + if c.branch not in changesets[candidate].branchpoints: + if p is not None: + break + continue + p = candidate + + c.parents = [] + if p is not None: + p = changesets[p] + + # Ensure no changeset has a synthetic changeset as a parent. + while p.synthetic: + assert len(p.parents) <= 1, \ + _('synthetic changeset cannot have multiple parents') + if p.parents: + p = p.parents[0] + else: + p = None + break + + if p is not None: + c.parents.append(p) + + if c.mergepoint: + if c.mergepoint == 'HEAD': + c.mergepoint = None + c.parents.append(changesets[branches[c.mergepoint]]) + + if mergefrom: + m = mergefrom.search(c.comment) + if m: + m = m.group(1) + if m == 'HEAD': + m = None + try: + candidate = changesets[branches[m]] + except KeyError: + ui.warn(_("warning: CVS commit message references " + "non-existent branch %r:\n%s\n") + % (m, c.comment)) + if m in branches and c.branch != m and not candidate.synthetic: + c.parents.append(candidate) + + if mergeto: + m = mergeto.search(c.comment) + if m: + try: + m = m.group(1) + if m == 'HEAD': + m = None + except: + m = None # if no group found then merge to HEAD + if m in branches and c.branch != m: + # insert empty changeset for merge + cc = changeset( + author=c.author, branch=m, date=c.date, + comment='convert-repo: CVS merge from branch %s' + % c.branch, + entries=[], tags=[], + parents=[changesets[branches[m]], c]) + changesets.insert(i + 1, cc) + branches[m] = i + 1 + + # adjust our loop counters now we have inserted a new entry + n += 1 + i += 2 + continue + + branches[c.branch] = i + i += 1 + + # Drop synthetic changesets (safe now that we have ensured no other + # changesets can have them as parents). + i = 0 + while i < len(changesets): + if changesets[i].synthetic: + del changesets[i] + else: + i += 1 + + # Number changesets + + for i, c in enumerate(changesets): + c.id = i + 1 + + ui.status(_('%d changeset entries\n') % len(changesets)) + + hook.hook(ui, None, "cvschangesets", True, changesets=changesets) + + return changesets + + +def debugcvsps(ui, *args, **opts): + '''Read CVS rlog for current directory or named path in + repository, and convert the log to changesets based on matching + commit log entries and dates. + ''' + if opts["new_cache"]: + cache = "write" + elif opts["update_cache"]: + cache = "update" + else: + cache = None + + revisions = opts["revisions"] + + try: + if args: + log = [] + for d in args: + log += createlog(ui, d, root=opts["root"], cache=cache) + else: + log = createlog(ui, root=opts["root"], cache=cache) + except logerror, e: + ui.write("%r\n"%e) + return + + changesets = createchangeset(ui, log, opts["fuzz"]) + del log + + # Print changesets (optionally filtered) + + off = len(revisions) + branches = {} # latest version number in each branch + ancestors = {} # parent branch + for cs in changesets: + + if opts["ancestors"]: + if cs.branch not in branches and cs.parents and cs.parents[0].id: + ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch, + cs.parents[0].id) + branches[cs.branch] = cs.id + + # limit by branches + if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]: + continue + + if not off: + # Note: trailing spaces on several lines here are needed to have + # bug-for-bug compatibility with cvsps. + ui.write('---------------------\n') + ui.write('PatchSet %d \n' % cs.id) + ui.write('Date: %s\n' % util.datestr(cs.date, + '%Y/%m/%d %H:%M:%S %1%2')) + ui.write('Author: %s\n' % cs.author) + ui.write('Branch: %s\n' % (cs.branch or 'HEAD')) + ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1], + ','.join(cs.tags) or '(none)')) + branchpoints = getattr(cs, 'branchpoints', None) + if branchpoints: + ui.write('Branchpoints: %s \n' % ', '.join(branchpoints)) + if opts["parents"] and cs.parents: + if len(cs.parents) > 1: + ui.write('Parents: %s\n' % + (','.join([str(p.id) for p in cs.parents]))) + else: + ui.write('Parent: %d\n' % cs.parents[0].id) + + if opts["ancestors"]: + b = cs.branch + r = [] + while b: + b, c = ancestors[b] + r.append('%s:%d:%d' % (b or "HEAD", c, branches[b])) + if r: + ui.write('Ancestors: %s\n' % (','.join(r))) + + ui.write('Log:\n') + ui.write('%s\n\n' % cs.comment) + ui.write('Members: \n') + for f in cs.entries: + fn = f.file + if fn.startswith(opts["prefix"]): + fn = fn[len(opts["prefix"]):] + ui.write('\t%s:%s->%s%s \n' % ( + fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL', + '.'.join([str(x) for x in f.revision]), + ['', '(DEAD)'][f.dead])) + ui.write('\n') + + # have we seen the start tag? + if revisions and off: + if revisions[0] == str(cs.id) or \ + revisions[0] in cs.tags: + off = False + + # see if we reached the end tag + if len(revisions) > 1 and not off: + if revisions[1] == str(cs.id) or \ + revisions[1] in cs.tags: + break diff --git a/plugins/hg4idea/testData/bin/hgext/convert/darcs.py b/plugins/hg4idea/testData/bin/hgext/convert/darcs.py new file mode 100644 index 000000000000..4e46aa16e7ab --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/darcs.py @@ -0,0 +1,167 @@ +# darcs.py - darcs support for the convert extension +# +# Copyright 2007-2009 Matt Mackall and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from common import NoRepo, checktool, commandline, commit, converter_source +from mercurial.i18n import _ +from mercurial import util +import os, shutil, tempfile + +# The naming drift of ElementTree is fun! + +try: + from xml.etree.cElementTree import ElementTree +except ImportError: + try: + from xml.etree.ElementTree import ElementTree + except ImportError: + try: + from elementtree.cElementTree import ElementTree + except ImportError: + try: + from elementtree.ElementTree import ElementTree + except ImportError: + ElementTree = None + +class darcs_source(converter_source, commandline): + def __init__(self, ui, path, rev=None): + converter_source.__init__(self, ui, path, rev=rev) + commandline.__init__(self, ui, 'darcs') + + # check for _darcs, ElementTree, _darcs/inventory so that we can + # easily skip test-convert-darcs if ElementTree is not around + if not os.path.exists(os.path.join(path, '_darcs', 'inventories')): + raise NoRepo(_("%s does not look like a darcs repository") % path) + + if not os.path.exists(os.path.join(path, '_darcs')): + raise NoRepo(_("%s does not look like a darcs repository") % path) + + checktool('darcs') + version = self.run0('--version').splitlines()[0].strip() + if version < '2.1': + raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') % + version) + + if ElementTree is None: + raise util.Abort(_("Python ElementTree module is not available")) + + self.path = os.path.realpath(path) + + self.lastrev = None + self.changes = {} + self.parents = {} + self.tags = {} + + def before(self): + self.tmppath = tempfile.mkdtemp( + prefix='convert-' + os.path.basename(self.path) + '-') + output, status = self.run('init', repodir=self.tmppath) + self.checkexit(status) + + tree = self.xml('changes', xml_output=True, summary=True, + repodir=self.path) + tagname = None + child = None + for elt in tree.findall('patch'): + node = elt.get('hash') + name = elt.findtext('name', '') + if name.startswith('TAG '): + tagname = name[4:].strip() + elif tagname is not None: + self.tags[tagname] = node + tagname = None + self.changes[node] = elt + self.parents[child] = [node] + child = node + self.parents[child] = [] + + def after(self): + self.ui.debug('cleaning up %s\n' % self.tmppath) + shutil.rmtree(self.tmppath, ignore_errors=True) + + def xml(self, cmd, **kwargs): + etree = ElementTree() + fp = self._run(cmd, **kwargs) + etree.parse(fp) + self.checkexit(fp.close()) + return etree.getroot() + + def manifest(self): + man = [] + output, status = self.run('show', 'files', no_directories=True, + repodir=self.tmppath) + self.checkexit(status) + for line in output.split('\n'): + path = line[2:] + if path: + man.append(path) + return man + + def getheads(self): + return self.parents[None] + + def getcommit(self, rev): + elt = self.changes[rev] + date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y') + desc = elt.findtext('name') + '\n' + elt.findtext('comment', '') + return commit(author=elt.get('author'), date=util.datestr(date), + desc=desc.strip(), parents=self.parents[rev]) + + def pull(self, rev): + output, status = self.run('pull', self.path, all=True, + match='hash %s' % rev, + no_test=True, no_posthook=True, + external_merge='/bin/false', + repodir=self.tmppath) + if status: + if output.find('We have conflicts in') == -1: + self.checkexit(status, output) + output, status = self.run('revert', all=True, repodir=self.tmppath) + self.checkexit(status, output) + + def getchanges(self, rev): + copies = {} + changes = [] + man = None + for elt in self.changes[rev].find('summary').getchildren(): + if elt.tag in ('add_directory', 'remove_directory'): + continue + if elt.tag == 'move': + if man is None: + man = self.manifest() + source, dest = elt.get('from'), elt.get('to') + if source in man: + # File move + changes.append((source, rev)) + changes.append((dest, rev)) + copies[dest] = source + else: + # Directory move, deduce file moves from manifest + source = source + '/' + for f in man: + if not f.startswith(source): + continue + fdest = dest + '/' + f[len(source):] + changes.append((f, rev)) + changes.append((fdest, rev)) + copies[fdest] = f + else: + changes.append((elt.text.strip(), rev)) + self.pull(rev) + self.lastrev = rev + return sorted(changes), copies + + def getfile(self, name, rev): + if rev != self.lastrev: + raise util.Abort(_('internal calling inconsistency')) + return open(os.path.join(self.tmppath, name), 'rb').read() + + def getmode(self, name, rev): + mode = os.lstat(os.path.join(self.tmppath, name)).st_mode + return (mode & 0111) and 'x' or '' + + def gettags(self): + return self.tags diff --git a/plugins/hg4idea/testData/bin/hgext/convert/filemap.py b/plugins/hg4idea/testData/bin/hgext/convert/filemap.py new file mode 100644 index 000000000000..e09a991a00c2 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/filemap.py @@ -0,0 +1,359 @@ +# Copyright 2007 Bryan O'Sullivan +# Copyright 2007 Alexis S. L. Carvalho +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import shlex +from mercurial.i18n import _ +from mercurial import util +from common import SKIPREV, converter_source + +def rpairs(name): + e = len(name) + while e != -1: + yield name[:e], name[e + 1:] + e = name.rfind('/', 0, e) + yield '.', name + +class filemapper(object): + '''Map and filter filenames when importing. + A name can be mapped to itself, a new name, or None (omit from new + repository).''' + + def __init__(self, ui, path=None): + self.ui = ui + self.include = {} + self.exclude = {} + self.rename = {} + if path: + if self.parse(path): + raise util.Abort(_('errors in filemap')) + + def parse(self, path): + errs = 0 + def check(name, mapping, listname): + if name in mapping: + self.ui.warn(_('%s:%d: %r already in %s list\n') % + (lex.infile, lex.lineno, name, listname)) + return 1 + return 0 + lex = shlex.shlex(open(path), path, True) + lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?' + cmd = lex.get_token() + while cmd: + if cmd == 'include': + name = lex.get_token() + errs += check(name, self.exclude, 'exclude') + self.include[name] = name + elif cmd == 'exclude': + name = lex.get_token() + errs += check(name, self.include, 'include') + errs += check(name, self.rename, 'rename') + self.exclude[name] = name + elif cmd == 'rename': + src = lex.get_token() + dest = lex.get_token() + errs += check(src, self.exclude, 'exclude') + self.rename[src] = dest + elif cmd == 'source': + errs += self.parse(lex.get_token()) + else: + self.ui.warn(_('%s:%d: unknown directive %r\n') % + (lex.infile, lex.lineno, cmd)) + errs += 1 + cmd = lex.get_token() + return errs + + def lookup(self, name, mapping): + for pre, suf in rpairs(name): + try: + return mapping[pre], pre, suf + except KeyError: + pass + return '', name, '' + + def __call__(self, name): + if self.include: + inc = self.lookup(name, self.include)[0] + else: + inc = name + if self.exclude: + exc = self.lookup(name, self.exclude)[0] + else: + exc = '' + if (not self.include and exc) or (len(inc) <= len(exc)): + return None + newpre, pre, suf = self.lookup(name, self.rename) + if newpre: + if newpre == '.': + return suf + if suf: + return newpre + '/' + suf + return newpre + return name + + def active(self): + return bool(self.include or self.exclude or self.rename) + +# This class does two additional things compared to a regular source: +# +# - Filter and rename files. This is mostly wrapped by the filemapper +# class above. We hide the original filename in the revision that is +# returned by getchanges to be able to find things later in getfile +# and getmode. +# +# - Return only revisions that matter for the files we're interested in. +# This involves rewriting the parents of the original revision to +# create a graph that is restricted to those revisions. +# +# This set of revisions includes not only revisions that directly +# touch files we're interested in, but also merges that merge two +# or more interesting revisions. + +class filemap_source(converter_source): + def __init__(self, ui, baseconverter, filemap): + super(filemap_source, self).__init__(ui) + self.base = baseconverter + self.filemapper = filemapper(ui, filemap) + self.commits = {} + # if a revision rev has parent p in the original revision graph, then + # rev will have parent self.parentmap[p] in the restricted graph. + self.parentmap = {} + # self.wantedancestors[rev] is the set of all ancestors of rev that + # are in the restricted graph. + self.wantedancestors = {} + self.convertedorder = None + self._rebuilt = False + self.origparents = {} + self.children = {} + self.seenchildren = {} + + def before(self): + self.base.before() + + def after(self): + self.base.after() + + def setrevmap(self, revmap): + # rebuild our state to make things restartable + # + # To avoid calling getcommit for every revision that has already + # been converted, we rebuild only the parentmap, delaying the + # rebuild of wantedancestors until we need it (i.e. until a + # merge). + # + # We assume the order argument lists the revisions in + # topological order, so that we can infer which revisions were + # wanted by previous runs. + self._rebuilt = not revmap + seen = {SKIPREV: SKIPREV} + dummyset = set() + converted = [] + for rev in revmap.order: + mapped = revmap[rev] + wanted = mapped not in seen + if wanted: + seen[mapped] = rev + self.parentmap[rev] = rev + else: + self.parentmap[rev] = seen[mapped] + self.wantedancestors[rev] = dummyset + arg = seen[mapped] + if arg == SKIPREV: + arg = None + converted.append((rev, wanted, arg)) + self.convertedorder = converted + return self.base.setrevmap(revmap) + + def rebuild(self): + if self._rebuilt: + return True + self._rebuilt = True + self.parentmap.clear() + self.wantedancestors.clear() + self.seenchildren.clear() + for rev, wanted, arg in self.convertedorder: + if rev not in self.origparents: + self.origparents[rev] = self.getcommit(rev).parents + if arg is not None: + self.children[arg] = self.children.get(arg, 0) + 1 + + for rev, wanted, arg in self.convertedorder: + parents = self.origparents[rev] + if wanted: + self.mark_wanted(rev, parents) + else: + self.mark_not_wanted(rev, arg) + self._discard(arg, *parents) + + return True + + def getheads(self): + return self.base.getheads() + + def getcommit(self, rev): + # We want to save a reference to the commit objects to be able + # to rewrite their parents later on. + c = self.commits[rev] = self.base.getcommit(rev) + for p in c.parents: + self.children[p] = self.children.get(p, 0) + 1 + return c + + def _discard(self, *revs): + for r in revs: + if r is None: + continue + self.seenchildren[r] = self.seenchildren.get(r, 0) + 1 + if self.seenchildren[r] == self.children[r]: + del self.wantedancestors[r] + del self.parentmap[r] + del self.seenchildren[r] + if self._rebuilt: + del self.children[r] + + def wanted(self, rev, i): + # Return True if we're directly interested in rev. + # + # i is an index selecting one of the parents of rev (if rev + # has no parents, i is None). getchangedfiles will give us + # the list of files that are different in rev and in the parent + # indicated by i. If we're interested in any of these files, + # we're interested in rev. + try: + files = self.base.getchangedfiles(rev, i) + except NotImplementedError: + raise util.Abort(_("source repository doesn't support --filemap")) + for f in files: + if self.filemapper(f): + return True + return False + + def mark_not_wanted(self, rev, p): + # Mark rev as not interesting and update data structures. + + if p is None: + # A root revision. Use SKIPREV to indicate that it doesn't + # map to any revision in the restricted graph. Put SKIPREV + # in the set of wanted ancestors to simplify code elsewhere + self.parentmap[rev] = SKIPREV + self.wantedancestors[rev] = set((SKIPREV,)) + return + + # Reuse the data from our parent. + self.parentmap[rev] = self.parentmap[p] + self.wantedancestors[rev] = self.wantedancestors[p] + + def mark_wanted(self, rev, parents): + # Mark rev ss wanted and update data structures. + + # rev will be in the restricted graph, so children of rev in + # the original graph should still have rev as a parent in the + # restricted graph. + self.parentmap[rev] = rev + + # The set of wanted ancestors of rev is the union of the sets + # of wanted ancestors of its parents. Plus rev itself. + wrev = set() + for p in parents: + wrev.update(self.wantedancestors[p]) + wrev.add(rev) + self.wantedancestors[rev] = wrev + + def getchanges(self, rev): + parents = self.commits[rev].parents + if len(parents) > 1: + self.rebuild() + + # To decide whether we're interested in rev we: + # + # - calculate what parents rev will have if it turns out we're + # interested in it. If it's going to have more than 1 parent, + # we're interested in it. + # + # - otherwise, we'll compare it with the single parent we found. + # If any of the files we're interested in is different in the + # the two revisions, we're interested in rev. + + # A parent p is interesting if its mapped version (self.parentmap[p]): + # - is not SKIPREV + # - is still not in the list of parents (we don't want duplicates) + # - is not an ancestor of the mapped versions of the other parents + mparents = [] + wp = None + for i, p1 in enumerate(parents): + mp1 = self.parentmap[p1] + if mp1 == SKIPREV or mp1 in mparents: + continue + for p2 in parents: + if p1 == p2 or mp1 == self.parentmap[p2]: + continue + if mp1 in self.wantedancestors[p2]: + break + else: + mparents.append(mp1) + wp = i + + if wp is None and parents: + wp = 0 + + self.origparents[rev] = parents + + if len(mparents) < 2 and not self.wanted(rev, wp): + # We don't want this revision. + # Update our state and tell the convert process to map this + # revision to the same revision its parent as mapped to. + p = None + if parents: + p = parents[wp] + self.mark_not_wanted(rev, p) + self.convertedorder.append((rev, False, p)) + self._discard(*parents) + return self.parentmap[rev] + + # We want this revision. + # Rewrite the parents of the commit object + self.commits[rev].parents = mparents + self.mark_wanted(rev, parents) + self.convertedorder.append((rev, True, None)) + self._discard(*parents) + + # Get the real changes and do the filtering/mapping. + # To be able to get the files later on in getfile and getmode, + # we hide the original filename in the rev part of the return + # value. + changes, copies = self.base.getchanges(rev) + newnames = {} + files = [] + for f, r in changes: + newf = self.filemapper(f) + if newf: + files.append((newf, (f, r))) + newnames[f] = newf + + ncopies = {} + for c in copies: + newc = self.filemapper(c) + if newc: + newsource = self.filemapper(copies[c]) + if newsource: + ncopies[newc] = newsource + + return files, ncopies + + def getfile(self, name, rev): + realname, realrev = rev + return self.base.getfile(realname, realrev) + + def getmode(self, name, rev): + realname, realrev = rev + return self.base.getmode(realname, realrev) + + def gettags(self): + return self.base.gettags() + + def hasnativeorder(self): + return self.base.hasnativeorder() + + def lookuprev(self, rev): + return self.base.lookuprev(rev) diff --git a/plugins/hg4idea/testData/bin/hgext/convert/git.py b/plugins/hg4idea/testData/bin/hgext/convert/git.py new file mode 100644 index 000000000000..ed08f01fef24 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/git.py @@ -0,0 +1,170 @@ +# git.py - git support for the convert extension +# +# Copyright 2005-2009 Matt Mackall and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import os +from mercurial import util +from mercurial.i18n import _ + +from common import NoRepo, commit, converter_source, checktool + +class convert_git(converter_source): + # Windows does not support GIT_DIR= construct while other systems + # cannot remove environment variable. Just assume none have + # both issues. + if hasattr(os, 'unsetenv'): + def gitopen(self, s): + prevgitdir = os.environ.get('GIT_DIR') + os.environ['GIT_DIR'] = self.path + try: + return util.popen(s, 'rb') + finally: + if prevgitdir is None: + del os.environ['GIT_DIR'] + else: + os.environ['GIT_DIR'] = prevgitdir + else: + def gitopen(self, s): + return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') + + def gitread(self, s): + fh = self.gitopen(s) + data = fh.read() + return data, fh.close() + + def __init__(self, ui, path, rev=None): + super(convert_git, self).__init__(ui, path, rev=rev) + + if os.path.isdir(path + "/.git"): + path += "/.git" + if not os.path.exists(path + "/objects"): + raise NoRepo(_("%s does not look like a Git repository") % path) + + checktool('git', 'git') + + self.path = path + + def getheads(self): + if not self.rev: + heads, ret = self.gitread('git rev-parse --branches --remotes') + heads = heads.splitlines() + else: + heads, ret = self.gitread("git rev-parse --verify %s" % self.rev) + heads = [heads[:-1]] + if ret: + raise util.Abort(_('cannot retrieve git heads')) + return heads + + def catfile(self, rev, type): + if rev == "0" * 40: + raise IOError() + data, ret = self.gitread("git cat-file %s %s" % (type, rev)) + if ret: + raise util.Abort(_('cannot read %r object at %s') % (type, rev)) + return data + + def getfile(self, name, rev): + return self.catfile(rev, "blob") + + def getmode(self, name, rev): + return self.modecache[(name, rev)] + + def getchanges(self, version): + self.modecache = {} + fh = self.gitopen("git diff-tree -z --root -m -r %s" % version) + changes = [] + seen = set() + entry = None + for l in fh.read().split('\x00'): + if not entry: + if not l.startswith(':'): + continue + entry = l + continue + f = l + if f not in seen: + seen.add(f) + entry = entry.split() + h = entry[3] + p = (entry[1] == "100755") + s = (entry[1] == "120000") + self.modecache[(f, h)] = (p and "x") or (s and "l") or "" + changes.append((f, h)) + entry = None + if fh.close(): + raise util.Abort(_('cannot read changes in %s') % version) + return (changes, {}) + + def getcommit(self, version): + c = self.catfile(version, "commit") # read the commit hash + end = c.find("\n\n") + message = c[end + 2:] + message = self.recode(message) + l = c[:end].splitlines() + parents = [] + author = committer = None + for e in l[1:]: + n, v = e.split(" ", 1) + if n == "author": + p = v.split() + tm, tz = p[-2:] + author = " ".join(p[:-2]) + if author[0] == "<": author = author[1:-1] + author = self.recode(author) + if n == "committer": + p = v.split() + tm, tz = p[-2:] + committer = " ".join(p[:-2]) + if committer[0] == "<": committer = committer[1:-1] + committer = self.recode(committer) + if n == "parent": + parents.append(v) + + if committer and committer != author: + message += "\ncommitter: %s\n" % committer + tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:] + tz = -int(tzs) * (int(tzh) * 3600 + int(tzm)) + date = tm + " " + str(tz) + + c = commit(parents=parents, date=date, author=author, desc=message, + rev=version) + return c + + def gettags(self): + tags = {} + fh = self.gitopen('git ls-remote --tags "%s"' % self.path) + prefix = 'refs/tags/' + for line in fh: + line = line.strip() + if not line.endswith("^{}"): + continue + node, tag = line.split(None, 1) + if not tag.startswith(prefix): + continue + tag = tag[len(prefix):-3] + tags[tag] = node + if fh.close(): + raise util.Abort(_('cannot read tags from %s') % self.path) + + return tags + + def getchangedfiles(self, version, i): + changes = [] + if i is None: + fh = self.gitopen("git diff-tree --root -m -r %s" % version) + for l in fh: + if "\t" not in l: + continue + m, f = l[:-1].split("\t") + changes.append(f) + else: + fh = self.gitopen('git diff-tree --name-only --root -r %s "%s^%s" --' + % (version, version, i + 1)) + changes = [f.rstrip('\n') for f in fh] + if fh.close(): + raise util.Abort(_('cannot read changes in %s') % version) + + return changes diff --git a/plugins/hg4idea/testData/bin/hgext/convert/gnuarch.py b/plugins/hg4idea/testData/bin/hgext/convert/gnuarch.py new file mode 100644 index 000000000000..2727772fb81d --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/gnuarch.py @@ -0,0 +1,346 @@ +# gnuarch.py - GNU Arch support for the convert extension +# +# Copyright 2008, 2009 Aleix Conchillo Flaque +# and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from common import NoRepo, commandline, commit, converter_source +from mercurial.i18n import _ +from mercurial import util +import os, shutil, tempfile, stat, locale +from email.Parser import Parser + +class gnuarch_source(converter_source, commandline): + + class gnuarch_rev(object): + def __init__(self, rev): + self.rev = rev + self.summary = '' + self.date = None + self.author = '' + self.continuationof = None + self.add_files = [] + self.mod_files = [] + self.del_files = [] + self.ren_files = {} + self.ren_dirs = {} + + def __init__(self, ui, path, rev=None): + super(gnuarch_source, self).__init__(ui, path, rev=rev) + + if not os.path.exists(os.path.join(path, '{arch}')): + raise NoRepo(_("%s does not look like a GNU Arch repository") + % path) + + # Could use checktool, but we want to check for baz or tla. + self.execmd = None + if util.find_exe('baz'): + self.execmd = 'baz' + else: + if util.find_exe('tla'): + self.execmd = 'tla' + else: + raise util.Abort(_('cannot find a GNU Arch tool')) + + commandline.__init__(self, ui, self.execmd) + + self.path = os.path.realpath(path) + self.tmppath = None + + self.treeversion = None + self.lastrev = None + self.changes = {} + self.parents = {} + self.tags = {} + self.modecache = {} + self.catlogparser = Parser() + self.locale = locale.getpreferredencoding() + self.archives = [] + + def before(self): + # Get registered archives + self.archives = [i.rstrip('\n') + for i in self.runlines0('archives', '-n')] + + if self.execmd == 'tla': + output = self.run0('tree-version', self.path) + else: + output = self.run0('tree-version', '-d', self.path) + self.treeversion = output.strip() + + # Get name of temporary directory + version = self.treeversion.split('/') + self.tmppath = os.path.join(tempfile.gettempdir(), + 'hg-%s' % version[1]) + + # Generate parents dictionary + self.parents[None] = [] + treeversion = self.treeversion + child = None + while treeversion: + self.ui.status(_('analyzing tree version %s...\n') % treeversion) + + archive = treeversion.split('/')[0] + if archive not in self.archives: + self.ui.status(_('tree analysis stopped because it points to ' + 'an unregistered archive %s...\n') % archive) + break + + # Get the complete list of revisions for that tree version + output, status = self.runlines('revisions', '-r', '-f', treeversion) + self.checkexit(status, 'failed retrieveing revisions for %s' + % treeversion) + + # No new iteration unless a revision has a continuation-of header + treeversion = None + + for l in output: + rev = l.strip() + self.changes[rev] = self.gnuarch_rev(rev) + self.parents[rev] = [] + + # Read author, date and summary + catlog, status = self.run('cat-log', '-d', self.path, rev) + if status: + catlog = self.run0('cat-archive-log', rev) + self._parsecatlog(catlog, rev) + + # Populate the parents map + self.parents[child].append(rev) + + # Keep track of the current revision as the child of the next + # revision scanned + child = rev + + # Check if we have to follow the usual incremental history + # or if we have to 'jump' to a different treeversion given + # by the continuation-of header. + if self.changes[rev].continuationof: + treeversion = '--'.join( + self.changes[rev].continuationof.split('--')[:-1]) + break + + # If we reached a base-0 revision w/o any continuation-of + # header, it means the tree history ends here. + if rev[-6:] == 'base-0': + break + + def after(self): + self.ui.debug('cleaning up %s\n' % self.tmppath) + shutil.rmtree(self.tmppath, ignore_errors=True) + + def getheads(self): + return self.parents[None] + + def getfile(self, name, rev): + if rev != self.lastrev: + raise util.Abort(_('internal calling inconsistency')) + + # Raise IOError if necessary (i.e. deleted files). + if not os.path.exists(os.path.join(self.tmppath, name)): + raise IOError + + data, mode = self._getfile(name, rev) + self.modecache[(name, rev)] = mode + + return data + + def getmode(self, name, rev): + return self.modecache[(name, rev)] + + def getchanges(self, rev): + self.modecache = {} + self._update(rev) + changes = [] + copies = {} + + for f in self.changes[rev].add_files: + changes.append((f, rev)) + + for f in self.changes[rev].mod_files: + changes.append((f, rev)) + + for f in self.changes[rev].del_files: + changes.append((f, rev)) + + for src in self.changes[rev].ren_files: + to = self.changes[rev].ren_files[src] + changes.append((src, rev)) + changes.append((to, rev)) + copies[to] = src + + for src in self.changes[rev].ren_dirs: + to = self.changes[rev].ren_dirs[src] + chgs, cps = self._rendirchanges(src, to) + changes += [(f, rev) for f in chgs] + copies.update(cps) + + self.lastrev = rev + return sorted(set(changes)), copies + + def getcommit(self, rev): + changes = self.changes[rev] + return commit(author=changes.author, date=changes.date, + desc=changes.summary, parents=self.parents[rev], rev=rev) + + def gettags(self): + return self.tags + + def _execute(self, cmd, *args, **kwargs): + cmdline = [self.execmd, cmd] + cmdline += args + cmdline = [util.shellquote(arg) for arg in cmdline] + cmdline += ['>', util.nulldev, '2>', util.nulldev] + cmdline = util.quotecommand(' '.join(cmdline)) + self.ui.debug(cmdline, '\n') + return os.system(cmdline) + + def _update(self, rev): + self.ui.debug('applying revision %s...\n' % rev) + changeset, status = self.runlines('replay', '-d', self.tmppath, + rev) + if status: + # Something went wrong while merging (baz or tla + # issue?), get latest revision and try from there + shutil.rmtree(self.tmppath, ignore_errors=True) + self._obtainrevision(rev) + else: + old_rev = self.parents[rev][0] + self.ui.debug('computing changeset between %s and %s...\n' + % (old_rev, rev)) + self._parsechangeset(changeset, rev) + + def _getfile(self, name, rev): + mode = os.lstat(os.path.join(self.tmppath, name)).st_mode + if stat.S_ISLNK(mode): + data = os.readlink(os.path.join(self.tmppath, name)) + mode = mode and 'l' or '' + else: + data = open(os.path.join(self.tmppath, name), 'rb').read() + mode = (mode & 0111) and 'x' or '' + return data, mode + + def _exclude(self, name): + exclude = ['{arch}', '.arch-ids', '.arch-inventory'] + for exc in exclude: + if name.find(exc) != -1: + return True + return False + + def _readcontents(self, path): + files = [] + contents = os.listdir(path) + while len(contents) > 0: + c = contents.pop() + p = os.path.join(path, c) + # os.walk could be used, but here we avoid internal GNU + # Arch files and directories, thus saving a lot time. + if not self._exclude(p): + if os.path.isdir(p): + contents += [os.path.join(c, f) for f in os.listdir(p)] + else: + files.append(c) + return files + + def _rendirchanges(self, src, dest): + changes = [] + copies = {} + files = self._readcontents(os.path.join(self.tmppath, dest)) + for f in files: + s = os.path.join(src, f) + d = os.path.join(dest, f) + changes.append(s) + changes.append(d) + copies[d] = s + return changes, copies + + def _obtainrevision(self, rev): + self.ui.debug('obtaining revision %s...\n' % rev) + output = self._execute('get', rev, self.tmppath) + self.checkexit(output) + self.ui.debug('analyzing revision %s...\n' % rev) + files = self._readcontents(self.tmppath) + self.changes[rev].add_files += files + + def _stripbasepath(self, path): + if path.startswith('./'): + return path[2:] + return path + + def _parsecatlog(self, data, rev): + try: + catlog = self.catlogparser.parsestr(data) + + # Commit date + self.changes[rev].date = util.datestr( + util.strdate(catlog['Standard-date'], + '%Y-%m-%d %H:%M:%S')) + + # Commit author + self.changes[rev].author = self.recode(catlog['Creator']) + + # Commit description + self.changes[rev].summary = '\n\n'.join((catlog['Summary'], + catlog.get_payload())) + self.changes[rev].summary = self.recode(self.changes[rev].summary) + + # Commit revision origin when dealing with a branch or tag + if 'Continuation-of' in catlog: + self.changes[rev].continuationof = self.recode( + catlog['Continuation-of']) + except Exception: + raise util.Abort(_('could not parse cat-log of %s') % rev) + + def _parsechangeset(self, data, rev): + for l in data: + l = l.strip() + # Added file (ignore added directory) + if l.startswith('A') and not l.startswith('A/'): + file = self._stripbasepath(l[1:].strip()) + if not self._exclude(file): + self.changes[rev].add_files.append(file) + # Deleted file (ignore deleted directory) + elif l.startswith('D') and not l.startswith('D/'): + file = self._stripbasepath(l[1:].strip()) + if not self._exclude(file): + self.changes[rev].del_files.append(file) + # Modified binary file + elif l.startswith('Mb'): + file = self._stripbasepath(l[2:].strip()) + if not self._exclude(file): + self.changes[rev].mod_files.append(file) + # Modified link + elif l.startswith('M->'): + file = self._stripbasepath(l[3:].strip()) + if not self._exclude(file): + self.changes[rev].mod_files.append(file) + # Modified file + elif l.startswith('M'): + file = self._stripbasepath(l[1:].strip()) + if not self._exclude(file): + self.changes[rev].mod_files.append(file) + # Renamed file (or link) + elif l.startswith('=>'): + files = l[2:].strip().split(' ') + if len(files) == 1: + files = l[2:].strip().split('\t') + src = self._stripbasepath(files[0]) + dst = self._stripbasepath(files[1]) + if not self._exclude(src) and not self._exclude(dst): + self.changes[rev].ren_files[src] = dst + # Conversion from file to link or from link to file (modified) + elif l.startswith('ch'): + file = self._stripbasepath(l[2:].strip()) + if not self._exclude(file): + self.changes[rev].mod_files.append(file) + # Renamed directory + elif l.startswith('/>'): + dirs = l[2:].strip().split(' ') + if len(dirs) == 1: + dirs = l[2:].strip().split('\t') + src = self._stripbasepath(dirs[0]) + dst = self._stripbasepath(dirs[1]) + if not self._exclude(src) and not self._exclude(dst): + self.changes[rev].ren_dirs[src] = dst diff --git a/plugins/hg4idea/testData/bin/hgext/convert/hg.py b/plugins/hg4idea/testData/bin/hgext/convert/hg.py new file mode 100644 index 000000000000..2a3d1376bf41 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/hg.py @@ -0,0 +1,377 @@ +# hg.py - hg backend for convert extension +# +# Copyright 2005-2009 Matt Mackall and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# Notes for hg->hg conversion: +# +# * Old versions of Mercurial didn't trim the whitespace from the ends +# of commit messages, but new versions do. Changesets created by +# those older versions, then converted, may thus have different +# hashes for changesets that are otherwise identical. +# +# * Using "--config convert.hg.saverev=true" will make the source +# identifier to be stored in the converted revision. This will cause +# the converted revision to have a different identity than the +# source. + + +import os, time, cStringIO +from mercurial.i18n import _ +from mercurial.node import bin, hex, nullid +from mercurial import hg, util, context, error + +from common import NoRepo, commit, converter_source, converter_sink + +class mercurial_sink(converter_sink): + def __init__(self, ui, path): + converter_sink.__init__(self, ui, path) + self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True) + self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False) + self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default') + self.lastbranch = None + if os.path.isdir(path) and len(os.listdir(path)) > 0: + try: + self.repo = hg.repository(self.ui, path) + if not self.repo.local(): + raise NoRepo(_('%s is not a local Mercurial repository') + % path) + except error.RepoError, err: + ui.traceback() + raise NoRepo(err.args[0]) + else: + try: + ui.status(_('initializing destination %s repository\n') % path) + self.repo = hg.repository(self.ui, path, create=True) + if not self.repo.local(): + raise NoRepo(_('%s is not a local Mercurial repository') + % path) + self.created.append(path) + except error.RepoError: + ui.traceback() + raise NoRepo(_("could not create hg repository %s as sink") + % path) + self.lock = None + self.wlock = None + self.filemapmode = False + + def before(self): + self.ui.debug('run hg sink pre-conversion action\n') + self.wlock = self.repo.wlock() + self.lock = self.repo.lock() + + def after(self): + self.ui.debug('run hg sink post-conversion action\n') + if self.lock: + self.lock.release() + if self.wlock: + self.wlock.release() + + def revmapfile(self): + return os.path.join(self.path, ".hg", "shamap") + + def authorfile(self): + return os.path.join(self.path, ".hg", "authormap") + + def getheads(self): + h = self.repo.changelog.heads() + return [hex(x) for x in h] + + def setbranch(self, branch, pbranches): + if not self.clonebranches: + return + + setbranch = (branch != self.lastbranch) + self.lastbranch = branch + if not branch: + branch = 'default' + pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches] + pbranch = pbranches and pbranches[0][1] or 'default' + + branchpath = os.path.join(self.path, branch) + if setbranch: + self.after() + try: + self.repo = hg.repository(self.ui, branchpath) + except: + self.repo = hg.repository(self.ui, branchpath, create=True) + self.before() + + # pbranches may bring revisions from other branches (merge parents) + # Make sure we have them, or pull them. + missings = {} + for b in pbranches: + try: + self.repo.lookup(b[0]) + except: + missings.setdefault(b[1], []).append(b[0]) + + if missings: + self.after() + for pbranch, heads in missings.iteritems(): + pbranchpath = os.path.join(self.path, pbranch) + prepo = hg.repository(self.ui, pbranchpath) + self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch)) + self.repo.pull(prepo, [prepo.lookup(h) for h in heads]) + self.before() + + def _rewritetags(self, source, revmap, data): + fp = cStringIO.StringIO() + for line in data.splitlines(): + s = line.split(' ', 1) + if len(s) != 2: + continue + revid = revmap.get(source.lookuprev(s[0])) + if not revid: + continue + fp.write('%s %s\n' % (revid, s[1])) + return fp.getvalue() + + def putcommit(self, files, copies, parents, commit, source, revmap): + + files = dict(files) + def getfilectx(repo, memctx, f): + v = files[f] + data = source.getfile(f, v) + e = source.getmode(f, v) + if f == '.hgtags': + data = self._rewritetags(source, revmap, data) + return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f)) + + pl = [] + for p in parents: + if p not in pl: + pl.append(p) + parents = pl + nparents = len(parents) + if self.filemapmode and nparents == 1: + m1node = self.repo.changelog.read(bin(parents[0]))[0] + parent = parents[0] + + if len(parents) < 2: + parents.append(nullid) + if len(parents) < 2: + parents.append(nullid) + p2 = parents.pop(0) + + text = commit.desc + extra = commit.extra.copy() + if self.branchnames and commit.branch: + extra['branch'] = commit.branch + if commit.rev: + extra['convert_revision'] = commit.rev + + while parents: + p1 = p2 + p2 = parents.pop(0) + ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), + getfilectx, commit.author, commit.date, extra) + self.repo.commitctx(ctx) + text = "(octopus merge fixup)\n" + p2 = hex(self.repo.changelog.tip()) + + if self.filemapmode and nparents == 1: + man = self.repo.manifest + mnode = self.repo.changelog.read(bin(p2))[0] + if not man.cmp(m1node, man.revision(mnode)): + self.ui.status(_("filtering out empty revision\n")) + self.repo.rollback() + return parent + return p2 + + def puttags(self, tags): + try: + parentctx = self.repo[self.tagsbranch] + tagparent = parentctx.node() + except error.RepoError: + parentctx = None + tagparent = nullid + + try: + oldlines = sorted(parentctx['.hgtags'].data().splitlines(True)) + except: + oldlines = [] + + newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags]) + if newlines == oldlines: + return None, None + data = "".join(newlines) + def getfilectx(repo, memctx, f): + return context.memfilectx(f, data, False, False, None) + + self.ui.status(_("updating tags\n")) + date = "%s 0" % int(time.mktime(time.gmtime())) + extra = {'branch': self.tagsbranch} + ctx = context.memctx(self.repo, (tagparent, None), "update tags", + [".hgtags"], getfilectx, "convert-repo", date, + extra) + self.repo.commitctx(ctx) + return hex(self.repo.changelog.tip()), hex(tagparent) + + def setfilemapmode(self, active): + self.filemapmode = active + +class mercurial_source(converter_source): + def __init__(self, ui, path, rev=None): + converter_source.__init__(self, ui, path, rev) + self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False) + self.ignored = set() + self.saverev = ui.configbool('convert', 'hg.saverev', False) + try: + self.repo = hg.repository(self.ui, path) + # try to provoke an exception if this isn't really a hg + # repo, but some other bogus compatible-looking url + if not self.repo.local(): + raise error.RepoError() + except error.RepoError: + ui.traceback() + raise NoRepo(_("%s is not a local Mercurial repository") % path) + self.lastrev = None + self.lastctx = None + self._changescache = None + self.convertfp = None + # Restrict converted revisions to startrev descendants + startnode = ui.config('convert', 'hg.startrev') + if startnode is not None: + try: + startnode = self.repo.lookup(startnode) + except error.RepoError: + raise util.Abort(_('%s is not a valid start revision') + % startnode) + startrev = self.repo.changelog.rev(startnode) + children = {startnode: 1} + for rev in self.repo.changelog.descendants(startrev): + children[self.repo.changelog.node(rev)] = 1 + self.keep = children.__contains__ + else: + self.keep = util.always + + def changectx(self, rev): + if self.lastrev != rev: + self.lastctx = self.repo[rev] + self.lastrev = rev + return self.lastctx + + def parents(self, ctx): + return [p for p in ctx.parents() if p and self.keep(p.node())] + + def getheads(self): + if self.rev: + heads = [self.repo[self.rev].node()] + else: + heads = self.repo.heads() + return [hex(h) for h in heads if self.keep(h)] + + def getfile(self, name, rev): + try: + return self.changectx(rev)[name].data() + except error.LookupError, err: + raise IOError(err) + + def getmode(self, name, rev): + return self.changectx(rev).manifest().flags(name) + + def getchanges(self, rev): + ctx = self.changectx(rev) + parents = self.parents(ctx) + if not parents: + files = sorted(ctx.manifest()) + if self.ignoreerrors: + # calling getcopies() is a simple way to detect missing + # revlogs and populate self.ignored + self.getcopies(ctx, parents, files) + return [(f, rev) for f in files if f not in self.ignored], {} + if self._changescache and self._changescache[0] == rev: + m, a, r = self._changescache[1] + else: + m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3] + # getcopies() detects missing revlogs early, run it before + # filtering the changes. + copies = self.getcopies(ctx, parents, m + a) + changes = [(name, rev) for name in m + a + r + if name not in self.ignored] + return sorted(changes), copies + + def getcopies(self, ctx, parents, files): + copies = {} + for name in files: + if name in self.ignored: + continue + try: + copysource, copynode = ctx.filectx(name).renamed() + if copysource in self.ignored or not self.keep(copynode): + continue + # Ignore copy sources not in parent revisions + found = False + for p in parents: + if copysource in p: + found = True + break + if not found: + continue + copies[name] = copysource + except TypeError: + pass + except error.LookupError, e: + if not self.ignoreerrors: + raise + self.ignored.add(name) + self.ui.warn(_('ignoring: %s\n') % e) + return copies + + def getcommit(self, rev): + ctx = self.changectx(rev) + parents = [p.hex() for p in self.parents(ctx)] + if self.saverev: + crev = rev + else: + crev = None + return commit(author=ctx.user(), date=util.datestr(ctx.date()), + desc=ctx.description(), rev=crev, parents=parents, + branch=ctx.branch(), extra=ctx.extra(), + sortkey=ctx.rev()) + + def gettags(self): + tags = [t for t in self.repo.tagslist() if t[0] != 'tip'] + return dict([(name, hex(node)) for name, node in tags + if self.keep(node)]) + + def getchangedfiles(self, rev, i): + ctx = self.changectx(rev) + parents = self.parents(ctx) + if not parents and i is None: + i = 0 + changes = [], ctx.manifest().keys(), [] + else: + i = i or 0 + changes = self.repo.status(parents[i].node(), ctx.node())[:3] + changes = [[f for f in l if f not in self.ignored] for l in changes] + + if i == 0: + self._changescache = (rev, changes) + + return changes[0] + changes[1] + changes[2] + + def converted(self, rev, destrev): + if self.convertfp is None: + self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'), + 'a') + self.convertfp.write('%s %s\n' % (destrev, rev)) + self.convertfp.flush() + + def before(self): + self.ui.debug('run hg source pre-conversion action\n') + + def after(self): + self.ui.debug('run hg source post-conversion action\n') + + def hasnativeorder(self): + return True + + def lookuprev(self, rev): + try: + return hex(self.repo.lookup(rev)) + except error.RepoError: + return None diff --git a/plugins/hg4idea/testData/bin/hgext/convert/monotone.py b/plugins/hg4idea/testData/bin/hgext/convert/monotone.py new file mode 100644 index 000000000000..e5ecd7556218 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/monotone.py @@ -0,0 +1,229 @@ +# monotone.py - monotone support for the convert extension +# +# Copyright 2008, 2009 Mikkel Fahnoe Jorgensen and +# others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import os, re +from mercurial import util +from common import NoRepo, commit, converter_source, checktool +from common import commandline +from mercurial.i18n import _ + +class monotone_source(converter_source, commandline): + def __init__(self, ui, path=None, rev=None): + converter_source.__init__(self, ui, path, rev) + commandline.__init__(self, ui, 'mtn') + + self.ui = ui + self.path = path + + norepo = NoRepo(_("%s does not look like a monotone repository") + % path) + if not os.path.exists(os.path.join(path, '_MTN')): + # Could be a monotone repository (SQLite db file) + try: + header = file(path, 'rb').read(16) + except: + header = '' + if header != 'SQLite format 3\x00': + raise norepo + + # regular expressions for parsing monotone output + space = r'\s*' + name = r'\s+"((?:\\"|[^"])*)"\s*' + value = name + revision = r'\s+\[(\w+)\]\s*' + lines = r'(?:.|\n)+' + + self.dir_re = re.compile(space + "dir" + name) + self.file_re = re.compile(space + "file" + name + + "content" + revision) + self.add_file_re = re.compile(space + "add_file" + name + + "content" + revision) + self.patch_re = re.compile(space + "patch" + name + + "from" + revision + "to" + revision) + self.rename_re = re.compile(space + "rename" + name + "to" + name) + self.delete_re = re.compile(space + "delete" + name) + self.tag_re = re.compile(space + "tag" + name + "revision" + + revision) + self.cert_re = re.compile(lines + space + "name" + name + + "value" + value) + + attr = space + "file" + lines + space + "attr" + space + self.attr_execute_re = re.compile(attr + '"mtn:execute"' + + space + '"true"') + + # cached data + self.manifest_rev = None + self.manifest = None + self.files = None + self.dirs = None + + checktool('mtn', abort=False) + + # test if there are any revisions + self.rev = None + try: + self.getheads() + except: + raise norepo + self.rev = rev + + def mtnrun(self, *args, **kwargs): + kwargs['d'] = self.path + return self.run0('automate', *args, **kwargs) + + def mtnloadmanifest(self, rev): + if self.manifest_rev == rev: + return + self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n") + self.manifest_rev = rev + self.files = {} + self.dirs = {} + + for e in self.manifest: + m = self.file_re.match(e) + if m: + attr = "" + name = m.group(1) + node = m.group(2) + if self.attr_execute_re.match(e): + attr += "x" + self.files[name] = (node, attr) + m = self.dir_re.match(e) + if m: + self.dirs[m.group(1)] = True + + def mtnisfile(self, name, rev): + # a non-file could be a directory or a deleted or renamed file + self.mtnloadmanifest(rev) + return name in self.files + + def mtnisdir(self, name, rev): + self.mtnloadmanifest(rev) + return name in self.dirs + + def mtngetcerts(self, rev): + certs = {"author":"", "date":"", + "changelog":"", "branch":""} + certlist = self.mtnrun("certs", rev) + # mtn < 0.45: + # key "test@selenic.com" + # mtn >= 0.45: + # key [ff58a7ffb771907c4ff68995eada1c4da068d328] + certlist = re.split('\n\n key ["\[]', certlist) + for e in certlist: + m = self.cert_re.match(e) + if m: + name, value = m.groups() + value = value.replace(r'\"', '"') + value = value.replace(r'\\', '\\') + certs[name] = value + # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306 + # and all times are stored in UTC + certs["date"] = certs["date"].split('.')[0] + " UTC" + return certs + + # implement the converter_source interface: + + def getheads(self): + if not self.rev: + return self.mtnrun("leaves").splitlines() + else: + return [self.rev] + + def getchanges(self, rev): + #revision = self.mtncmd("get_revision %s" % rev).split("\n\n") + revision = self.mtnrun("get_revision", rev).split("\n\n") + files = {} + ignoremove = {} + renameddirs = [] + copies = {} + for e in revision: + m = self.add_file_re.match(e) + if m: + files[m.group(1)] = rev + ignoremove[m.group(1)] = rev + m = self.patch_re.match(e) + if m: + files[m.group(1)] = rev + # Delete/rename is handled later when the convert engine + # discovers an IOError exception from getfile, + # but only if we add the "from" file to the list of changes. + m = self.delete_re.match(e) + if m: + files[m.group(1)] = rev + m = self.rename_re.match(e) + if m: + toname = m.group(2) + fromname = m.group(1) + if self.mtnisfile(toname, rev): + ignoremove[toname] = 1 + copies[toname] = fromname + files[toname] = rev + files[fromname] = rev + elif self.mtnisdir(toname, rev): + renameddirs.append((fromname, toname)) + + # Directory renames can be handled only once we have recorded + # all new files + for fromdir, todir in renameddirs: + renamed = {} + for tofile in self.files: + if tofile in ignoremove: + continue + if tofile.startswith(todir + '/'): + renamed[tofile] = fromdir + tofile[len(todir):] + # Avoid chained moves like: + # d1(/a) => d3/d1(/a) + # d2 => d3 + ignoremove[tofile] = 1 + for tofile, fromfile in renamed.items(): + self.ui.debug (_("copying file in renamed directory " + "from '%s' to '%s'") + % (fromfile, tofile), '\n') + files[tofile] = rev + copies[tofile] = fromfile + for fromfile in renamed.values(): + files[fromfile] = rev + + return (files.items(), copies) + + def getmode(self, name, rev): + self.mtnloadmanifest(rev) + node, attr = self.files.get(name, (None, "")) + return attr + + def getfile(self, name, rev): + if not self.mtnisfile(name, rev): + raise IOError() # file was deleted or renamed + try: + return self.mtnrun("get_file_of", name, r=rev) + except: + raise IOError() # file was deleted or renamed + + def getcommit(self, rev): + certs = self.mtngetcerts(rev) + return commit( + author=certs["author"], + date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")), + desc=certs["changelog"], + rev=rev, + parents=self.mtnrun("parents", rev).splitlines(), + branch=certs["branch"]) + + def gettags(self): + tags = {} + for e in self.mtnrun("tags").split("\n\n"): + m = self.tag_re.match(e) + if m: + tags[m.group(1)] = m.group(2) + return tags + + def getchangedfiles(self, rev, i): + # This function is only needed to support --filemap + # ... and we don't support that + raise NotImplementedError() diff --git a/plugins/hg4idea/testData/bin/hgext/convert/p4.py b/plugins/hg4idea/testData/bin/hgext/convert/p4.py new file mode 100644 index 000000000000..64967f24a59c --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/p4.py @@ -0,0 +1,208 @@ +# Perforce source for convert extension. +# +# Copyright 2009, Frank Kingswood +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from mercurial import util +from mercurial.i18n import _ + +from common import commit, converter_source, checktool, NoRepo +import marshal +import re + +def loaditer(f): + "Yield the dictionary objects generated by p4" + try: + while True: + d = marshal.load(f) + if not d: + break + yield d + except EOFError: + pass + +class p4_source(converter_source): + def __init__(self, ui, path, rev=None): + super(p4_source, self).__init__(ui, path, rev=rev) + + if "/" in path and not path.startswith('//'): + raise NoRepo(_('%s does not look like a P4 repository') % path) + + checktool('p4', abort=False) + + self.p4changes = {} + self.heads = {} + self.changeset = {} + self.files = {} + self.tags = {} + self.lastbranch = {} + self.parent = {} + self.encoding = "latin_1" + self.depotname = {} # mapping from local name to depot name + self.modecache = {} + self.re_type = re.compile( + "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)" + "(\+\w+)?$") + self.re_keywords = re.compile( + r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)" + r":[^$\n]*\$") + self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$") + + self._parse(ui, path) + + def _parse_view(self, path): + "Read changes affecting the path" + cmd = 'p4 -G changes -s submitted "%s"' % path + stdout = util.popen(cmd, mode='rb') + for d in loaditer(stdout): + c = d.get("change", None) + if c: + self.p4changes[c] = True + + def _parse(self, ui, path): + "Prepare list of P4 filenames and revisions to import" + ui.status(_('reading p4 views\n')) + + # read client spec or view + if "/" in path: + self._parse_view(path) + if path.startswith("//") and path.endswith("/..."): + views = {path[:-3]:""} + else: + views = {"//": ""} + else: + cmd = 'p4 -G client -o "%s"' % path + clientspec = marshal.load(util.popen(cmd, mode='rb')) + + views = {} + for client in clientspec: + if client.startswith("View"): + sview, cview = clientspec[client].split() + self._parse_view(sview) + if sview.endswith("...") and cview.endswith("..."): + sview = sview[:-3] + cview = cview[:-3] + cview = cview[2:] + cview = cview[cview.find("/") + 1:] + views[sview] = cview + + # list of changes that affect our source files + self.p4changes = self.p4changes.keys() + self.p4changes.sort(key=int) + + # list with depot pathnames, longest first + vieworder = views.keys() + vieworder.sort(key=len, reverse=True) + + # handle revision limiting + startrev = self.ui.config('convert', 'p4.startrev', default=0) + self.p4changes = [x for x in self.p4changes + if ((not startrev or int(x) >= int(startrev)) and + (not self.rev or int(x) <= int(self.rev)))] + + # now read the full changelists to get the list of file revisions + ui.status(_('collecting p4 changelists\n')) + lastid = None + for change in self.p4changes: + cmd = "p4 -G describe %s" % change + stdout = util.popen(cmd, mode='rb') + d = marshal.load(stdout) + + desc = self.recode(d["desc"]) + shortdesc = desc.split("\n", 1)[0] + t = '%s %s' % (d["change"], repr(shortdesc)[1:-1]) + ui.status(util.ellipsis(t, 80) + '\n') + + if lastid: + parents = [lastid] + else: + parents = [] + + date = (int(d["time"]), 0) # timezone not set + c = commit(author=self.recode(d["user"]), date=util.datestr(date), + parents=parents, desc=desc, branch='', + extra={"p4": change}) + + files = [] + i = 0 + while ("depotFile%d" % i) in d and ("rev%d" % i) in d: + oldname = d["depotFile%d" % i] + filename = None + for v in vieworder: + if oldname.startswith(v): + filename = views[v] + oldname[len(v):] + break + if filename: + files.append((filename, d["rev%d" % i])) + self.depotname[filename] = oldname + i += 1 + self.changeset[change] = c + self.files[change] = files + lastid = change + + if lastid: + self.heads = [lastid] + + def getheads(self): + return self.heads + + def getfile(self, name, rev): + cmd = 'p4 -G print "%s#%s"' % (self.depotname[name], rev) + stdout = util.popen(cmd, mode='rb') + + mode = None + contents = "" + keywords = None + + for d in loaditer(stdout): + code = d["code"] + data = d.get("data") + + if code == "error": + raise IOError(d["generic"], data) + + elif code == "stat": + p4type = self.re_type.match(d["type"]) + if p4type: + mode = "" + flags = (p4type.group(1) or "") + (p4type.group(3) or "") + if "x" in flags: + mode = "x" + if p4type.group(2) == "symlink": + mode = "l" + if "ko" in flags: + keywords = self.re_keywords_old + elif "k" in flags: + keywords = self.re_keywords + + elif code == "text" or code == "binary": + contents += data + + if mode is None: + raise IOError(0, "bad stat") + + self.modecache[(name, rev)] = mode + + if keywords: + contents = keywords.sub("$\\1$", contents) + if mode == "l" and contents.endswith("\n"): + contents = contents[:-1] + + return contents + + def getmode(self, name, rev): + return self.modecache[(name, rev)] + + def getchanges(self, rev): + return self.files[rev], {} + + def getcommit(self, rev): + return self.changeset[rev] + + def gettags(self): + return self.tags + + def getchangedfiles(self, rev, i): + return sorted([x[0] for x in self.files[rev]]) diff --git a/plugins/hg4idea/testData/bin/hgext/convert/subversion.py b/plugins/hg4idea/testData/bin/hgext/convert/subversion.py new file mode 100644 index 000000000000..d2ac80cc6d6a --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/subversion.py @@ -0,0 +1,1178 @@ +# Subversion 1.4/1.5 Python API backend +# +# Copyright(C) 2007 Daniel Holth et al + +import os +import re +import sys +import cPickle as pickle +import tempfile +import urllib +import urllib2 + +from mercurial import strutil, util, encoding +from mercurial.i18n import _ + +# Subversion stuff. Works best with very recent Python SVN bindings +# e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing +# these bindings. + +from cStringIO import StringIO + +from common import NoRepo, MissingTool, commit, encodeargs, decodeargs +from common import commandline, converter_source, converter_sink, mapfile + +try: + from svn.core import SubversionException, Pool + import svn + import svn.client + import svn.core + import svn.ra + import svn.delta + import transport + import warnings + warnings.filterwarnings('ignore', + module='svn.core', + category=DeprecationWarning) + +except ImportError: + pass + +class SvnPathNotFound(Exception): + pass + +def geturl(path): + try: + return svn.client.url_from_path(svn.core.svn_path_canonicalize(path)) + except SubversionException: + pass + if os.path.isdir(path): + path = os.path.normpath(os.path.abspath(path)) + if os.name == 'nt': + path = '/' + util.normpath(path) + # Module URL is later compared with the repository URL returned + # by svn API, which is UTF-8. + path = encoding.tolocal(path) + return 'file://%s' % urllib.quote(path) + return path + +def optrev(number): + optrev = svn.core.svn_opt_revision_t() + optrev.kind = svn.core.svn_opt_revision_number + optrev.value.number = number + return optrev + +class changedpath(object): + def __init__(self, p): + self.copyfrom_path = p.copyfrom_path + self.copyfrom_rev = p.copyfrom_rev + self.action = p.action + +def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True, + strict_node_history=False): + protocol = -1 + def receiver(orig_paths, revnum, author, date, message, pool): + if orig_paths is not None: + for k, v in orig_paths.iteritems(): + orig_paths[k] = changedpath(v) + pickle.dump((orig_paths, revnum, author, date, message), + fp, protocol) + + try: + # Use an ra of our own so that our parent can consume + # our results without confusing the server. + t = transport.SvnRaTransport(url=url) + svn.ra.get_log(t.ra, paths, start, end, limit, + discover_changed_paths, + strict_node_history, + receiver) + except SubversionException, (inst, num): + pickle.dump(num, fp, protocol) + except IOError: + # Caller may interrupt the iteration + pickle.dump(None, fp, protocol) + else: + pickle.dump(None, fp, protocol) + fp.close() + # With large history, cleanup process goes crazy and suddenly + # consumes *huge* amount of memory. The output file being closed, + # there is no need for clean termination. + os._exit(0) + +def debugsvnlog(ui, **opts): + """Fetch SVN log in a subprocess and channel them back to parent to + avoid memory collection issues. + """ + util.set_binary(sys.stdin) + util.set_binary(sys.stdout) + args = decodeargs(sys.stdin.read()) + get_log_child(sys.stdout, *args) + +class logstream(object): + """Interruptible revision log iterator.""" + def __init__(self, stdout): + self._stdout = stdout + + def __iter__(self): + while True: + try: + entry = pickle.load(self._stdout) + except EOFError: + raise util.Abort(_('Mercurial failed to run itself, check' + ' hg executable is in PATH')) + try: + orig_paths, revnum, author, date, message = entry + except: + if entry is None: + break + raise SubversionException("child raised exception", entry) + yield entry + + def close(self): + if self._stdout: + self._stdout.close() + self._stdout = None + + +# Check to see if the given path is a local Subversion repo. Verify this by +# looking for several svn-specific files and directories in the given +# directory. +def filecheck(ui, path, proto): + for x in ('locks', 'hooks', 'format', 'db'): + if not os.path.exists(os.path.join(path, x)): + return False + return True + +# Check to see if a given path is the root of an svn repo over http. We verify +# this by requesting a version-controlled URL we know can't exist and looking +# for the svn-specific "not found" XML. +def httpcheck(ui, path, proto): + try: + opener = urllib2.build_opener() + rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path)) + data = rsp.read() + except urllib2.HTTPError, inst: + if inst.code != 404: + # Except for 404 we cannot know for sure this is not an svn repo + ui.warn(_('svn: cannot probe remote repository, assume it could ' + 'be a subversion repository. Use --source-type if you ' + 'know better.\n')) + return True + data = inst.fp.read() + except: + # Could be urllib2.URLError if the URL is invalid or anything else. + return False + return '' in data + +protomap = {'http': httpcheck, + 'https': httpcheck, + 'file': filecheck, + } +def issvnurl(ui, url): + try: + proto, path = url.split('://', 1) + if proto == 'file': + path = urllib.url2pathname(path) + except ValueError: + proto = 'file' + path = os.path.abspath(url) + if proto == 'file': + path = path.replace(os.sep, '/') + check = protomap.get(proto, lambda *args: False) + while '/' in path: + if check(ui, path, proto): + return True + path = path.rsplit('/', 1)[0] + return False + +# SVN conversion code stolen from bzr-svn and tailor +# +# Subversion looks like a versioned filesystem, branches structures +# are defined by conventions and not enforced by the tool. First, +# we define the potential branches (modules) as "trunk" and "branches" +# children directories. Revisions are then identified by their +# module and revision number (and a repository identifier). +# +# The revision graph is really a tree (or a forest). By default, a +# revision parent is the previous revision in the same module. If the +# module directory is copied/moved from another module then the +# revision is the module root and its parent the source revision in +# the parent module. A revision has at most one parent. +# +class svn_source(converter_source): + def __init__(self, ui, url, rev=None): + super(svn_source, self).__init__(ui, url, rev=rev) + + if not (url.startswith('svn://') or url.startswith('svn+ssh://') or + (os.path.exists(url) and + os.path.exists(os.path.join(url, '.svn'))) or + issvnurl(ui, url)): + raise NoRepo(_("%s does not look like a Subversion repository") + % url) + + try: + SubversionException + except NameError: + raise MissingTool(_('Subversion python bindings could not be loaded')) + + try: + version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR + if version < (1, 4): + raise MissingTool(_('Subversion python bindings %d.%d found, ' + '1.4 or later required') % version) + except AttributeError: + raise MissingTool(_('Subversion python bindings are too old, 1.4 ' + 'or later required')) + + self.lastrevs = {} + + latest = None + try: + # Support file://path@rev syntax. Useful e.g. to convert + # deleted branches. + at = url.rfind('@') + if at >= 0: + latest = int(url[at + 1:]) + url = url[:at] + except ValueError: + pass + self.url = geturl(url) + self.encoding = 'UTF-8' # Subversion is always nominal UTF-8 + try: + self.transport = transport.SvnRaTransport(url=self.url) + self.ra = self.transport.ra + self.ctx = self.transport.client + self.baseurl = svn.ra.get_repos_root(self.ra) + # Module is either empty or a repository path starting with + # a slash and not ending with a slash. + self.module = urllib.unquote(self.url[len(self.baseurl):]) + self.prevmodule = None + self.rootmodule = self.module + self.commits = {} + self.paths = {} + self.uuid = svn.ra.get_uuid(self.ra) + except SubversionException: + ui.traceback() + raise NoRepo(_("%s does not look like a Subversion repository") + % self.url) + + if rev: + try: + latest = int(rev) + except ValueError: + raise util.Abort(_('svn: revision %s is not an integer') % rev) + + self.startrev = self.ui.config('convert', 'svn.startrev', default=0) + try: + self.startrev = int(self.startrev) + if self.startrev < 0: + self.startrev = 0 + except ValueError: + raise util.Abort(_('svn: start revision %s is not an integer') + % self.startrev) + + self.head = self.latest(self.module, latest) + if not self.head: + raise util.Abort(_('no revision found in module %s') + % self.module) + self.last_changed = self.revnum(self.head) + + self._changescache = None + + if os.path.exists(os.path.join(url, '.svn/entries')): + self.wc = url + else: + self.wc = None + self.convertfp = None + + def setrevmap(self, revmap): + lastrevs = {} + for revid in revmap.iterkeys(): + uuid, module, revnum = self.revsplit(revid) + lastrevnum = lastrevs.setdefault(module, revnum) + if revnum > lastrevnum: + lastrevs[module] = revnum + self.lastrevs = lastrevs + + def exists(self, path, optrev): + try: + svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path), + optrev, False, self.ctx) + return True + except SubversionException: + return False + + def getheads(self): + + def isdir(path, revnum): + kind = self._checkpath(path, revnum) + return kind == svn.core.svn_node_dir + + def getcfgpath(name, rev): + cfgpath = self.ui.config('convert', 'svn.' + name) + if cfgpath is not None and cfgpath.strip() == '': + return None + path = (cfgpath or name).strip('/') + if not self.exists(path, rev): + if cfgpath: + raise util.Abort(_('expected %s to be at %r, but not found') + % (name, path)) + return None + self.ui.note(_('found %s at %r\n') % (name, path)) + return path + + rev = optrev(self.last_changed) + oldmodule = '' + trunk = getcfgpath('trunk', rev) + self.tags = getcfgpath('tags', rev) + branches = getcfgpath('branches', rev) + + # If the project has a trunk or branches, we will extract heads + # from them. We keep the project root otherwise. + if trunk: + oldmodule = self.module or '' + self.module += '/' + trunk + self.head = self.latest(self.module, self.last_changed) + if not self.head: + raise util.Abort(_('no revision found in module %s') + % self.module) + + # First head in the list is the module's head + self.heads = [self.head] + if self.tags is not None: + self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags')) + + # Check if branches bring a few more heads to the list + if branches: + rpath = self.url.strip('/') + branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches), + rev, False, self.ctx) + for branch in branchnames.keys(): + module = '%s/%s/%s' % (oldmodule, branches, branch) + if not isdir(module, self.last_changed): + continue + brevid = self.latest(module, self.last_changed) + if not brevid: + self.ui.note(_('ignoring empty branch %s\n') % branch) + continue + self.ui.note(_('found branch %s at %d\n') % + (branch, self.revnum(brevid))) + self.heads.append(brevid) + + if self.startrev and self.heads: + if len(self.heads) > 1: + raise util.Abort(_('svn: start revision is not supported ' + 'with more than one branch')) + revnum = self.revnum(self.heads[0]) + if revnum < self.startrev: + raise util.Abort( + _('svn: no revision found after start revision %d') + % self.startrev) + + return self.heads + + def getfile(self, file, rev): + data, mode = self._getfile(file, rev) + self.modecache[(file, rev)] = mode + return data + + def getmode(self, file, rev): + return self.modecache[(file, rev)] + + def getchanges(self, rev): + if self._changescache and self._changescache[0] == rev: + return self._changescache[1] + self._changescache = None + self.modecache = {} + (paths, parents) = self.paths[rev] + if parents: + files, self.removed, copies = self.expandpaths(rev, paths, parents) + else: + # Perform a full checkout on roots + uuid, module, revnum = self.revsplit(rev) + entries = svn.client.ls(self.baseurl + urllib.quote(module), + optrev(revnum), True, self.ctx) + files = [n for n, e in entries.iteritems() + if e.kind == svn.core.svn_node_file] + copies = {} + self.removed = set() + + files.sort() + files = zip(files, [rev] * len(files)) + + # caller caches the result, so free it here to release memory + del self.paths[rev] + return (files, copies) + + def getchangedfiles(self, rev, i): + changes = self.getchanges(rev) + self._changescache = (rev, changes) + return [f[0] for f in changes[0]] + + def getcommit(self, rev): + if rev not in self.commits: + uuid, module, revnum = self.revsplit(rev) + self.module = module + self.reparent(module) + # We assume that: + # - requests for revisions after "stop" come from the + # revision graph backward traversal. Cache all of them + # down to stop, they will be used eventually. + # - requests for revisions before "stop" come to get + # isolated branches parents. Just fetch what is needed. + stop = self.lastrevs.get(module, 0) + if revnum < stop: + stop = revnum + 1 + self._fetch_revisions(revnum, stop) + commit = self.commits[rev] + # caller caches the result, so free it here to release memory + del self.commits[rev] + return commit + + def gettags(self): + tags = {} + if self.tags is None: + return tags + + # svn tags are just a convention, project branches left in a + # 'tags' directory. There is no other relationship than + # ancestry, which is expensive to discover and makes them hard + # to update incrementally. Worse, past revisions may be + # referenced by tags far away in the future, requiring a deep + # history traversal on every calculation. Current code + # performs a single backward traversal, tracking moves within + # the tags directory (tag renaming) and recording a new tag + # everytime a project is copied from outside the tags + # directory. It also lists deleted tags, this behaviour may + # change in the future. + pendings = [] + tagspath = self.tags + start = svn.ra.get_latest_revnum(self.ra) + try: + for entry in self._getlog([self.tags], start, self.startrev): + origpaths, revnum, author, date, message = entry + copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e + in origpaths.iteritems() if e.copyfrom_path] + # Apply moves/copies from more specific to general + copies.sort(reverse=True) + + srctagspath = tagspath + if copies and copies[-1][2] == tagspath: + # Track tags directory moves + srctagspath = copies.pop()[0] + + for source, sourcerev, dest in copies: + if not dest.startswith(tagspath + '/'): + continue + for tag in pendings: + if tag[0].startswith(dest): + tagpath = source + tag[0][len(dest):] + tag[:2] = [tagpath, sourcerev] + break + else: + pendings.append([source, sourcerev, dest]) + + # Filter out tags with children coming from different + # parts of the repository like: + # /tags/tag.1 (from /trunk:10) + # /tags/tag.1/foo (from /branches/foo:12) + # Here/tags/tag.1 discarded as well as its children. + # It happens with tools like cvs2svn. Such tags cannot + # be represented in mercurial. + addeds = dict((p, e.copyfrom_path) for p, e + in origpaths.iteritems() + if e.action == 'A' and e.copyfrom_path) + badroots = set() + for destroot in addeds: + for source, sourcerev, dest in pendings: + if (not dest.startswith(destroot + '/') + or source.startswith(addeds[destroot] + '/')): + continue + badroots.add(destroot) + break + + for badroot in badroots: + pendings = [p for p in pendings if p[2] != badroot + and not p[2].startswith(badroot + '/')] + + # Tell tag renamings from tag creations + remainings = [] + for source, sourcerev, dest in pendings: + tagname = dest.split('/')[-1] + if source.startswith(srctagspath): + remainings.append([source, sourcerev, tagname]) + continue + if tagname in tags: + # Keep the latest tag value + continue + # From revision may be fake, get one with changes + try: + tagid = self.latest(source, sourcerev) + if tagid and tagname not in tags: + tags[tagname] = tagid + except SvnPathNotFound: + # It happens when we are following directories + # we assumed were copied with their parents + # but were really created in the tag + # directory. + pass + pendings = remainings + tagspath = srctagspath + + except SubversionException: + self.ui.note(_('no tags found at revision %d\n') % start) + return tags + + def converted(self, rev, destrev): + if not self.wc: + return + if self.convertfp is None: + self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'), + 'a') + self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev))) + self.convertfp.flush() + + def revid(self, revnum, module=None): + return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum) + + def revnum(self, rev): + return int(rev.split('@')[-1]) + + def revsplit(self, rev): + url, revnum = rev.rsplit('@', 1) + revnum = int(revnum) + parts = url.split('/', 1) + uuid = parts.pop(0)[4:] + mod = '' + if parts: + mod = '/' + parts[0] + return uuid, mod, revnum + + def latest(self, path, stop=0): + """Find the latest revid affecting path, up to stop. It may return + a revision in a different module, since a branch may be moved without + a change being reported. Return None if computed module does not + belong to rootmodule subtree. + """ + if not path.startswith(self.rootmodule): + # Requests on foreign branches may be forbidden at server level + self.ui.debug('ignoring foreign branch %r\n' % path) + return None + + if not stop: + stop = svn.ra.get_latest_revnum(self.ra) + try: + prevmodule = self.reparent('') + dirent = svn.ra.stat(self.ra, path.strip('/'), stop) + self.reparent(prevmodule) + except SubversionException: + dirent = None + if not dirent: + raise SvnPathNotFound(_('%s not found up to revision %d') + % (path, stop)) + + # stat() gives us the previous revision on this line of + # development, but it might be in *another module*. Fetch the + # log and detect renames down to the latest revision. + stream = self._getlog([path], stop, dirent.created_rev) + try: + for entry in stream: + paths, revnum, author, date, message = entry + if revnum <= dirent.created_rev: + break + + for p in paths: + if not path.startswith(p) or not paths[p].copyfrom_path: + continue + newpath = paths[p].copyfrom_path + path[len(p):] + self.ui.debug("branch renamed from %s to %s at %d\n" % + (path, newpath, revnum)) + path = newpath + break + finally: + stream.close() + + if not path.startswith(self.rootmodule): + self.ui.debug('ignoring foreign branch %r\n' % path) + return None + return self.revid(dirent.created_rev, path) + + def reparent(self, module): + """Reparent the svn transport and return the previous parent.""" + if self.prevmodule == module: + return module + svnurl = self.baseurl + urllib.quote(module) + prevmodule = self.prevmodule + if prevmodule is None: + prevmodule = '' + self.ui.debug("reparent to %s\n" % svnurl) + svn.ra.reparent(self.ra, svnurl) + self.prevmodule = module + return prevmodule + + def expandpaths(self, rev, paths, parents): + changed, removed = set(), set() + # Map of entrypath, revision for finding source of deleted + # revisions. + copyfrom = {} + copies = {} + + new_module, revnum = self.revsplit(rev)[1:] + if new_module != self.module: + self.module = new_module + self.reparent(self.module) + + for path, ent in paths: + entrypath = self.getrelpath(path) + + kind = self._checkpath(entrypath, revnum) + if kind == svn.core.svn_node_file: + changed.add(self.recode(entrypath)) + if not ent.copyfrom_path or not parents: + continue + # Copy sources not in parent revisions cannot be + # represented, ignore their origin for now + pmodule, prevnum = self.revsplit(parents[0])[1:] + if ent.copyfrom_rev < prevnum: + continue + copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule) + if not copyfrom_path: + continue + self.ui.debug("copied to %s from %s@%s\n" % + (entrypath, copyfrom_path, ent.copyfrom_rev)) + copies[self.recode(entrypath)] = self.recode(copyfrom_path) + elif kind == 0: # gone, but had better be a deleted *file* + self.ui.debug("gone from %s\n" % ent.copyfrom_rev) + pmodule, prevnum = self.revsplit(parents[0])[1:] + parentpath = pmodule + "/" + entrypath + fromkind = self._checkpath(entrypath, prevnum, pmodule) + + if fromkind == svn.core.svn_node_file: + removed.add(self.recode(entrypath)) + elif fromkind == svn.core.svn_node_dir: + oroot = parentpath.strip('/') + nroot = path.strip('/') + children = self._find_children(oroot, prevnum) + children = [s.replace(oroot, nroot) for s in children] + for child in children: + childpath = self.getrelpath("/" + child, pmodule) + if childpath: + removed.add(self.recode(childpath)) + else: + self.ui.debug('unknown path in revision %d: %s\n' % \ + (revnum, path)) + elif kind == svn.core.svn_node_dir: + if ent.action == 'M': + # If the directory just had a prop change, + # then we shouldn't need to look for its children. + continue + elif ent.action == 'R' and parents: + # If a directory is replacing a file, mark the previous + # file as deleted + pmodule, prevnum = self.revsplit(parents[0])[1:] + pkind = self._checkpath(entrypath, prevnum, pmodule) + if pkind == svn.core.svn_node_file: + removed.add(self.recode(entrypath)) + + children = sorted(self._find_children(path, revnum)) + for child in children: + # Can we move a child directory and its + # parent in the same commit? (probably can). Could + # cause problems if instead of revnum -1, + # we have to look in (copyfrom_path, revnum - 1) + entrypath = self.getrelpath("/" + child) + if entrypath: + # Need to filter out directories here... + kind = self._checkpath(entrypath, revnum) + if kind != svn.core.svn_node_dir: + changed.add(self.recode(entrypath)) + + # Handle directory copies + if not ent.copyfrom_path or not parents: + continue + # Copy sources not in parent revisions cannot be + # represented, ignore their origin for now + pmodule, prevnum = self.revsplit(parents[0])[1:] + if ent.copyfrom_rev < prevnum: + continue + copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule) + if not copyfrompath: + continue + copyfrom[path] = ent + self.ui.debug("mark %s came from %s:%d\n" + % (path, copyfrompath, ent.copyfrom_rev)) + children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev) + children.sort() + for child in children: + entrypath = self.getrelpath("/" + child, pmodule) + if not entrypath: + continue + copytopath = path + entrypath[len(copyfrompath):] + copytopath = self.getrelpath(copytopath) + copies[self.recode(copytopath)] = self.recode(entrypath) + + changed.update(removed) + return (list(changed), removed, copies) + + def _fetch_revisions(self, from_revnum, to_revnum): + if from_revnum < to_revnum: + from_revnum, to_revnum = to_revnum, from_revnum + + self.child_cset = None + + def parselogentry(orig_paths, revnum, author, date, message): + """Return the parsed commit object or None, and True if + the revision is a branch root. + """ + self.ui.debug("parsing revision %d (%d changes)\n" % + (revnum, len(orig_paths))) + + branched = False + rev = self.revid(revnum) + # branch log might return entries for a parent we already have + + if rev in self.commits or revnum < to_revnum: + return None, branched + + parents = [] + # check whether this revision is the start of a branch or part + # of a branch renaming + orig_paths = sorted(orig_paths.iteritems()) + root_paths = [(p, e) for p, e in orig_paths + if self.module.startswith(p)] + if root_paths: + path, ent = root_paths[-1] + if ent.copyfrom_path: + branched = True + newpath = ent.copyfrom_path + self.module[len(path):] + # ent.copyfrom_rev may not be the actual last revision + previd = self.latest(newpath, ent.copyfrom_rev) + if previd is not None: + prevmodule, prevnum = self.revsplit(previd)[1:] + if prevnum >= self.startrev: + parents = [previd] + self.ui.note( + _('found parent of branch %s at %d: %s\n') % + (self.module, prevnum, prevmodule)) + else: + self.ui.debug("no copyfrom path, don't know what to do.\n") + + paths = [] + # filter out unrelated paths + for path, ent in orig_paths: + if self.getrelpath(path) is None: + continue + paths.append((path, ent)) + + # Example SVN datetime. Includes microseconds. + # ISO-8601 conformant + # '2007-01-04T17:35:00.902377Z' + date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"]) + + log = message and self.recode(message) or '' + author = author and self.recode(author) or '' + try: + branch = self.module.split("/")[-1] + if branch == 'trunk': + branch = '' + except IndexError: + branch = None + + cset = commit(author=author, + date=util.datestr(date), + desc=log, + parents=parents, + branch=branch, + rev=rev) + + self.commits[rev] = cset + # The parents list is *shared* among self.paths and the + # commit object. Both will be updated below. + self.paths[rev] = (paths, cset.parents) + if self.child_cset and not self.child_cset.parents: + self.child_cset.parents[:] = [rev] + self.child_cset = cset + return cset, branched + + self.ui.note(_('fetching revision log for "%s" from %d to %d\n') % + (self.module, from_revnum, to_revnum)) + + try: + firstcset = None + lastonbranch = False + stream = self._getlog([self.module], from_revnum, to_revnum) + try: + for entry in stream: + paths, revnum, author, date, message = entry + if revnum < self.startrev: + lastonbranch = True + break + if not paths: + self.ui.debug('revision %d has no entries\n' % revnum) + # If we ever leave the loop on an empty + # revision, do not try to get a parent branch + lastonbranch = lastonbranch or revnum == 0 + continue + cset, lastonbranch = parselogentry(paths, revnum, author, + date, message) + if cset: + firstcset = cset + if lastonbranch: + break + finally: + stream.close() + + if not lastonbranch and firstcset and not firstcset.parents: + # The first revision of the sequence (the last fetched one) + # has invalid parents if not a branch root. Find the parent + # revision now, if any. + try: + firstrevnum = self.revnum(firstcset.rev) + if firstrevnum > 1: + latest = self.latest(self.module, firstrevnum - 1) + if latest: + firstcset.parents.append(latest) + except SvnPathNotFound: + pass + except SubversionException, (inst, num): + if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION: + raise util.Abort(_('svn: branch has no revision %s') % to_revnum) + raise + + def _getfile(self, file, rev): + # TODO: ra.get_file transmits the whole file instead of diffs. + if file in self.removed: + raise IOError() + mode = '' + try: + new_module, revnum = self.revsplit(rev)[1:] + if self.module != new_module: + self.module = new_module + self.reparent(self.module) + io = StringIO() + info = svn.ra.get_file(self.ra, file, revnum, io) + data = io.getvalue() + # ra.get_files() seems to keep a reference on the input buffer + # preventing collection. Release it explicitely. + io.close() + if isinstance(info, list): + info = info[-1] + mode = ("svn:executable" in info) and 'x' or '' + mode = ("svn:special" in info) and 'l' or mode + except SubversionException, e: + notfound = (svn.core.SVN_ERR_FS_NOT_FOUND, + svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND) + if e.apr_err in notfound: # File not found + raise IOError() + raise + if mode == 'l': + link_prefix = "link " + if data.startswith(link_prefix): + data = data[len(link_prefix):] + return data, mode + + def _find_children(self, path, revnum): + path = path.strip('/') + pool = Pool() + rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/') + return ['%s/%s' % (path, x) for x in + svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()] + + def getrelpath(self, path, module=None): + if module is None: + module = self.module + # Given the repository url of this wc, say + # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" + # extract the "entry" portion (a relative path) from what + # svn log --xml says, ie + # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py" + # that is to say "tests/PloneTestCase.py" + if path.startswith(module): + relative = path.rstrip('/')[len(module):] + if relative.startswith('/'): + return relative[1:] + elif relative == '': + return relative + + # The path is outside our tracked tree... + self.ui.debug('%r is not under %r, ignoring\n' % (path, module)) + return None + + def _checkpath(self, path, revnum, module=None): + if module is not None: + prevmodule = self.reparent('') + path = module + '/' + path + try: + # ra.check_path does not like leading slashes very much, it leads + # to PROPFIND subversion errors + return svn.ra.check_path(self.ra, path.strip('/'), revnum) + finally: + if module is not None: + self.reparent(prevmodule) + + def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True, + strict_node_history=False): + # Normalize path names, svn >= 1.5 only wants paths relative to + # supplied URL + relpaths = [] + for p in paths: + if not p.startswith('/'): + p = self.module + '/' + p + relpaths.append(p.strip('/')) + args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths, + strict_node_history] + arg = encodeargs(args) + hgexe = util.hgexecutable() + cmd = '%s debugsvnlog' % util.shellquote(hgexe) + stdin, stdout = util.popen2(cmd) + stdin.write(arg) + try: + stdin.close() + except IOError: + raise util.Abort(_('Mercurial failed to run itself, check' + ' hg executable is in PATH')) + return logstream(stdout) + +pre_revprop_change = '''#!/bin/sh + +REPOS="$1" +REV="$2" +USER="$3" +PROPNAME="$4" +ACTION="$5" + +if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi + +echo "Changing prohibited revision property" >&2 +exit 1 +''' + +class svn_sink(converter_sink, commandline): + commit_re = re.compile(r'Committed revision (\d+).', re.M) + + def prerun(self): + if self.wc: + os.chdir(self.wc) + + def postrun(self): + if self.wc: + os.chdir(self.cwd) + + def join(self, name): + return os.path.join(self.wc, '.svn', name) + + def revmapfile(self): + return self.join('hg-shamap') + + def authorfile(self): + return self.join('hg-authormap') + + def __init__(self, ui, path): + converter_sink.__init__(self, ui, path) + commandline.__init__(self, ui, 'svn') + self.delete = [] + self.setexec = [] + self.delexec = [] + self.copies = [] + self.wc = None + self.cwd = os.getcwd() + + path = os.path.realpath(path) + + created = False + if os.path.isfile(os.path.join(path, '.svn', 'entries')): + self.wc = path + self.run0('update') + else: + wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc') + + if os.path.isdir(os.path.dirname(path)): + if not os.path.exists(os.path.join(path, 'db', 'fs-type')): + ui.status(_('initializing svn repository %r\n') % + os.path.basename(path)) + commandline(ui, 'svnadmin').run0('create', path) + created = path + path = util.normpath(path) + if not path.startswith('/'): + path = '/' + path + path = 'file://' + path + + ui.status(_('initializing svn working copy %r\n') + % os.path.basename(wcpath)) + self.run0('checkout', path, wcpath) + + self.wc = wcpath + self.opener = util.opener(self.wc) + self.wopener = util.opener(self.wc) + self.childmap = mapfile(ui, self.join('hg-childmap')) + self.is_exec = util.checkexec(self.wc) and util.is_exec or None + + if created: + hook = os.path.join(created, 'hooks', 'pre-revprop-change') + fp = open(hook, 'w') + fp.write(pre_revprop_change) + fp.close() + util.set_flags(hook, False, True) + + xport = transport.SvnRaTransport(url=geturl(path)) + self.uuid = svn.ra.get_uuid(xport.ra) + + def wjoin(self, *names): + return os.path.join(self.wc, *names) + + def putfile(self, filename, flags, data): + if 'l' in flags: + self.wopener.symlink(data, filename) + else: + try: + if os.path.islink(self.wjoin(filename)): + os.unlink(filename) + except OSError: + pass + self.wopener(filename, 'w').write(data) + + if self.is_exec: + was_exec = self.is_exec(self.wjoin(filename)) + else: + # On filesystems not supporting execute-bit, there is no way + # to know if it is set but asking subversion. Setting it + # systematically is just as expensive and much simpler. + was_exec = 'x' not in flags + + util.set_flags(self.wjoin(filename), False, 'x' in flags) + if was_exec: + if 'x' not in flags: + self.delexec.append(filename) + else: + if 'x' in flags: + self.setexec.append(filename) + + def _copyfile(self, source, dest): + # SVN's copy command pukes if the destination file exists, but + # our copyfile method expects to record a copy that has + # already occurred. Cross the semantic gap. + wdest = self.wjoin(dest) + exists = os.path.exists(wdest) + if exists: + fd, tempname = tempfile.mkstemp( + prefix='hg-copy-', dir=os.path.dirname(wdest)) + os.close(fd) + os.unlink(tempname) + os.rename(wdest, tempname) + try: + self.run0('copy', source, dest) + finally: + if exists: + try: + os.unlink(wdest) + except OSError: + pass + os.rename(tempname, wdest) + + def dirs_of(self, files): + dirs = set() + for f in files: + if os.path.isdir(self.wjoin(f)): + dirs.add(f) + for i in strutil.rfindall(f, '/'): + dirs.add(f[:i]) + return dirs + + def add_dirs(self, files): + add_dirs = [d for d in sorted(self.dirs_of(files)) + if not os.path.exists(self.wjoin(d, '.svn', 'entries'))] + if add_dirs: + self.xargs(add_dirs, 'add', non_recursive=True, quiet=True) + return add_dirs + + def add_files(self, files): + if files: + self.xargs(files, 'add', quiet=True) + return files + + def tidy_dirs(self, names): + deleted = [] + for d in sorted(self.dirs_of(names), reverse=True): + wd = self.wjoin(d) + if os.listdir(wd) == '.svn': + self.run0('delete', d) + deleted.append(d) + return deleted + + def addchild(self, parent, child): + self.childmap[parent] = child + + def revid(self, rev): + return u"svn:%s@%s" % (self.uuid, rev) + + def putcommit(self, files, copies, parents, commit, source, revmap): + # Apply changes to working copy + for f, v in files: + try: + data = source.getfile(f, v) + except IOError: + self.delete.append(f) + else: + e = source.getmode(f, v) + self.putfile(f, e, data) + if f in copies: + self.copies.append([copies[f], f]) + files = [f[0] for f in files] + + for parent in parents: + try: + return self.revid(self.childmap[parent]) + except KeyError: + pass + entries = set(self.delete) + files = frozenset(files) + entries.update(self.add_dirs(files.difference(entries))) + if self.copies: + for s, d in self.copies: + self._copyfile(s, d) + self.copies = [] + if self.delete: + self.xargs(self.delete, 'delete') + self.delete = [] + entries.update(self.add_files(files.difference(entries))) + entries.update(self.tidy_dirs(entries)) + if self.delexec: + self.xargs(self.delexec, 'propdel', 'svn:executable') + self.delexec = [] + if self.setexec: + self.xargs(self.setexec, 'propset', 'svn:executable', '*') + self.setexec = [] + + fd, messagefile = tempfile.mkstemp(prefix='hg-convert-') + fp = os.fdopen(fd, 'w') + fp.write(commit.desc) + fp.close() + try: + output = self.run0('commit', + username=util.shortuser(commit.author), + file=messagefile, + encoding='utf-8') + try: + rev = self.commit_re.search(output).group(1) + except AttributeError: + if not files: + return parents[0] + self.ui.warn(_('unexpected svn output:\n')) + self.ui.warn(output) + raise util.Abort(_('unable to cope with svn output')) + if commit.rev: + self.run('propset', 'hg:convert-rev', commit.rev, + revprop=True, revision=rev) + if commit.branch and commit.branch != 'default': + self.run('propset', 'hg:convert-branch', commit.branch, + revprop=True, revision=rev) + for parent in parents: + self.addchild(parent, rev) + return self.revid(rev) + finally: + os.unlink(messagefile) + + def puttags(self, tags): + self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n')) diff --git a/plugins/hg4idea/testData/bin/hgext/convert/transport.py b/plugins/hg4idea/testData/bin/hgext/convert/transport.py new file mode 100644 index 000000000000..77cba414ddb8 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/convert/transport.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2007 Daniel Holth +# This is a stripped-down version of the original bzr-svn transport.py, +# Copyright (C) 2006 Jelmer Vernooij + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from svn.core import SubversionException, Pool +import svn.ra +import svn.client +import svn.core + +# Some older versions of the Python bindings need to be +# explicitly initialized. But what we want to do probably +# won't work worth a darn against those libraries anyway! +svn.ra.initialize() + +svn_config = svn.core.svn_config_get_config(None) + + +def _create_auth_baton(pool): + """Create a Subversion authentication baton. """ + import svn.client + # Give the client context baton a suite of authentication + # providers.h + providers = [ + svn.client.get_simple_provider(pool), + svn.client.get_username_provider(pool), + svn.client.get_ssl_client_cert_file_provider(pool), + svn.client.get_ssl_client_cert_pw_file_provider(pool), + svn.client.get_ssl_server_trust_file_provider(pool), + ] + # Platform-dependant authentication methods + getprovider = getattr(svn.core, 'svn_auth_get_platform_specific_provider', + None) + if getprovider: + # Available in svn >= 1.6 + for name in ('gnome_keyring', 'keychain', 'kwallet', 'windows'): + for type in ('simple', 'ssl_client_cert_pw', 'ssl_server_trust'): + p = getprovider(name, type, pool) + if p: + providers.append(p) + else: + if hasattr(svn.client, 'get_windows_simple_provider'): + providers.append(svn.client.get_windows_simple_provider(pool)) + + return svn.core.svn_auth_open(providers, pool) + +class NotBranchError(SubversionException): + pass + +class SvnRaTransport(object): + """ + Open an ra connection to a Subversion repository. + """ + def __init__(self, url="", ra=None): + self.pool = Pool() + self.svn_url = url + self.username = '' + self.password = '' + + # Only Subversion 1.4 has reparent() + if ra is None or not hasattr(svn.ra, 'reparent'): + self.client = svn.client.create_context(self.pool) + ab = _create_auth_baton(self.pool) + if False: + svn.core.svn_auth_set_parameter( + ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username) + svn.core.svn_auth_set_parameter( + ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password) + self.client.auth_baton = ab + self.client.config = svn_config + try: + self.ra = svn.client.open_ra_session( + self.svn_url.encode('utf8'), + self.client, self.pool) + except SubversionException, (inst, num): + if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL, + svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED, + svn.core.SVN_ERR_BAD_URL): + raise NotBranchError(url) + raise + else: + self.ra = ra + svn.ra.reparent(self.ra, self.svn_url.encode('utf8')) + + class Reporter(object): + def __init__(self, (reporter, report_baton)): + self._reporter = reporter + self._baton = report_baton + + def set_path(self, path, revnum, start_empty, lock_token, pool=None): + svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, + path, revnum, start_empty, lock_token, pool) + + def delete_path(self, path, pool=None): + svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton, + path, pool) + + def link_path(self, path, url, revision, start_empty, lock_token, + pool=None): + svn.ra.reporter2_invoke_link_path(self._reporter, self._baton, + path, url, revision, start_empty, lock_token, + pool) + + def finish_report(self, pool=None): + svn.ra.reporter2_invoke_finish_report(self._reporter, + self._baton, pool) + + def abort_report(self, pool=None): + svn.ra.reporter2_invoke_abort_report(self._reporter, + self._baton, pool) + + def do_update(self, revnum, path, *args, **kwargs): + return self.Reporter(svn.ra.do_update(self.ra, revnum, path, + *args, **kwargs)) diff --git a/plugins/hg4idea/testData/bin/hgext/extdiff.py b/plugins/hg4idea/testData/bin/hgext/extdiff.py new file mode 100644 index 000000000000..6fca94a13c15 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/extdiff.py @@ -0,0 +1,283 @@ +# extdiff.py - external diff program support for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to allow external programs to compare revisions + +The extdiff Mercurial extension allows you to use external programs +to compare revisions, or revision with working directory. The external +diff programs are called with a configurable set of options and two +non-option arguments: paths to directories containing snapshots of +files to compare. + +The extdiff extension also allows to configure new diff commands, so +you do not need to type "hg extdiff -p kdiff3" always. :: + + [extdiff] + # add new command that runs GNU diff(1) in 'context diff' mode + cdiff = gdiff -Nprc5 + ## or the old way: + #cmd.cdiff = gdiff + #opts.cdiff = -Nprc5 + + # add new command called vdiff, runs kdiff3 + vdiff = kdiff3 + + # add new command called meld, runs meld (no need to name twice) + meld = + + # add new command called vimdiff, runs gvimdiff with DirDiff plugin + # (see http://www.vim.org/scripts/script.php?script_id=102) Non + # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in + # your .vimrc + vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)' + +You can use -I/-X and list of file or directory names like normal "hg +diff" command. The extdiff extension makes snapshots of only needed +files, so running the external diff program will actually be pretty +fast (at least faster than having to compare the entire tree). +''' + +from mercurial.i18n import _ +from mercurial.node import short, nullid +from mercurial import cmdutil, util, commands, encoding +import os, shlex, shutil, tempfile, re + +def snapshot(ui, repo, files, node, tmproot): + '''snapshot files as of some revision + if not using snapshot, -I/-X does not work and recursive diff + in tools like kdiff3 and meld displays too many files.''' + dirname = os.path.basename(repo.root) + if dirname == "": + dirname = "root" + if node is not None: + dirname = '%s.%s' % (dirname, short(node)) + base = os.path.join(tmproot, dirname) + os.mkdir(base) + if node is not None: + ui.note(_('making snapshot of %d files from rev %s\n') % + (len(files), short(node))) + else: + ui.note(_('making snapshot of %d files from working directory\n') % + (len(files))) + wopener = util.opener(base) + fns_and_mtime = [] + ctx = repo[node] + for fn in files: + wfn = util.pconvert(fn) + if not wfn in ctx: + # File doesn't exist; could be a bogus modify + continue + ui.note(' %s\n' % wfn) + dest = os.path.join(base, wfn) + fctx = ctx[wfn] + data = repo.wwritedata(wfn, fctx.data()) + if 'l' in fctx.flags(): + wopener.symlink(data, wfn) + else: + wopener(wfn, 'w').write(data) + if 'x' in fctx.flags(): + util.set_flags(dest, False, True) + if node is None: + fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest))) + return dirname, fns_and_mtime + +def dodiff(ui, repo, diffcmd, diffopts, pats, opts): + '''Do the actuall diff: + + - copy to a temp structure if diffing 2 internal revisions + - copy to a temp structure if diffing working revision with + another one and more than 1 file is changed + - just invoke the diff for a single file in the working dir + ''' + + revs = opts.get('rev') + change = opts.get('change') + args = ' '.join(diffopts) + do3way = '$parent2' in args + + if revs and change: + msg = _('cannot specify --rev and --change at the same time') + raise util.Abort(msg) + elif change: + node2 = repo.lookup(change) + node1a, node1b = repo.changelog.parents(node2) + else: + node1a, node2 = cmdutil.revpair(repo, revs) + if not revs: + node1b = repo.dirstate.parents()[1] + else: + node1b = nullid + + # Disable 3-way merge if there is only one parent + if do3way: + if node1b == nullid: + do3way = False + + matcher = cmdutil.match(repo, pats, opts) + mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3]) + if do3way: + mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3]) + else: + mod_b, add_b, rem_b = set(), set(), set() + modadd = mod_a | add_a | mod_b | add_b + common = modadd | rem_a | rem_b + if not common: + return 0 + + tmproot = tempfile.mkdtemp(prefix='extdiff.') + try: + # Always make a copy of node1a (and node1b, if applicable) + dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a) + dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0] + if do3way: + dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b) + dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0] + else: + dir1b = None + + fns_and_mtime = [] + + # If node2 in not the wc or there is >1 change, copy it + dir2root = '' + if node2: + dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0] + elif len(common) > 1: + #we only actually need to get the files to copy back to + #the working dir in this case (because the other cases + #are: diffing 2 revisions or single file -- in which case + #the file is already directly passed to the diff tool). + dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot) + else: + # This lets the diff tool open the changed file directly + dir2 = '' + dir2root = repo.root + + # If only one change, diff the files instead of the directories + # Handle bogus modifies correctly by checking if the files exist + if len(common) == 1: + common_file = util.localpath(common.pop()) + dir1a = os.path.join(dir1a, common_file) + if not os.path.isfile(os.path.join(tmproot, dir1a)): + dir1a = os.devnull + if do3way: + dir1b = os.path.join(dir1b, common_file) + if not os.path.isfile(os.path.join(tmproot, dir1b)): + dir1b = os.devnull + dir2 = os.path.join(dir2root, dir2, common_file) + + # Function to quote file/dir names in the argument string. + # When not operating in 3-way mode, an empty string is + # returned for parent2 + replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, child=dir2) + def quote(match): + key = match.group()[1:] + if not do3way and key == 'parent2': + return '' + return util.shellquote(replace[key]) + + # Match parent2 first, so 'parent1?' will match both parent1 and parent + regex = '\$(parent2|parent1?|child)' + if not do3way and not re.search(regex, args): + args += ' $parent1 $child' + args = re.sub(regex, quote, args) + cmdline = util.shellquote(diffcmd) + ' ' + args + + ui.debug('running %r in %s\n' % (cmdline, tmproot)) + util.system(cmdline, cwd=tmproot) + + for copy_fn, working_fn, mtime in fns_and_mtime: + if os.path.getmtime(copy_fn) != mtime: + ui.debug('file changed while diffing. ' + 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)) + util.copyfile(copy_fn, working_fn) + + return 1 + finally: + ui.note(_('cleaning up temp directory\n')) + shutil.rmtree(tmproot) + +def extdiff(ui, repo, *pats, **opts): + '''use external program to diff repository (or selected files) + + Show differences between revisions for the specified files, using + an external program. The default program used is diff, with + default options "-Npru". + + To select a different program, use the -p/--program option. The + program will be passed the names of two directories to compare. To + pass additional options to the program, use -o/--option. These + will be passed before the names of the directories to compare. + + When two revision arguments are given, then changes are shown + between those revisions. If only one revision is specified then + that revision is compared to the working directory, and, when no + revisions are specified, the working directory files are compared + to its parent.''' + program = opts.get('program') + option = opts.get('option') + if not program: + program = 'diff' + option = option or ['-Npru'] + return dodiff(ui, repo, program, option, pats, opts) + +cmdtable = { + "extdiff": + (extdiff, + [('p', 'program', '', _('comparison program to run')), + ('o', 'option', [], _('pass option to comparison program')), + ('r', 'rev', [], _('revision')), + ('c', 'change', '', _('change made by revision')), + ] + commands.walkopts, + _('hg extdiff [OPT]... [FILE]...')), + } + +def uisetup(ui): + for cmd, path in ui.configitems('extdiff'): + if cmd.startswith('cmd.'): + cmd = cmd[4:] + if not path: + path = cmd + diffopts = ui.config('extdiff', 'opts.' + cmd, '') + diffopts = diffopts and [diffopts] or [] + elif cmd.startswith('opts.'): + continue + else: + # command = path opts + if path: + diffopts = shlex.split(path) + path = diffopts.pop(0) + else: + path, diffopts = cmd, [] + def save(cmd, path, diffopts): + '''use closure to save diff command to use''' + def mydiff(ui, repo, *pats, **opts): + return dodiff(ui, repo, path, diffopts + opts['option'], + pats, opts) + doc = _('''\ +use %(path)s to diff repository (or selected files) + + Show differences between revisions for the specified files, using + the %(path)s program. + + When two revision arguments are given, then changes are shown + between those revisions. If only one revision is specified then + that revision is compared to the working directory, and, when no + revisions are specified, the working directory files are compared + to its parent.\ +''') % dict(path=util.uirepr(path)) + + # We must translate the docstring right away since it is + # used as a format string. The string will unfortunately + # be translated again in commands.helpcmd and this will + # fail when the docstring contains non-ASCII characters. + # Decoding the string to a Unicode string here (using the + # right encoding) prevents that. + mydiff.__doc__ = doc.decode(encoding.encoding) + return mydiff + cmdtable[cmd] = (save(cmd, path, diffopts), + cmdtable['extdiff'][1][1:], + _('hg %s [OPTION]... [FILE]...') % cmd) diff --git a/plugins/hg4idea/testData/bin/hgext/fetch.py b/plugins/hg4idea/testData/bin/hgext/fetch.py new file mode 100644 index 000000000000..337686dca697 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/fetch.py @@ -0,0 +1,147 @@ +# fetch.py - pull and merge remote changes +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''pull, update and merge in one command''' + +from mercurial.i18n import _ +from mercurial.node import nullid, short +from mercurial import commands, cmdutil, hg, util, url, error +from mercurial.lock import release + +def fetch(ui, repo, source='default', **opts): + '''pull changes from a remote repository, merge new changes if needed. + + This finds all changes from the repository at the specified path + or URL and adds them to the local repository. + + If the pulled changes add a new branch head, the head is + automatically merged, and the result of the merge is committed. + Otherwise, the working directory is updated to include the new + changes. + + When a merge occurs, the newly pulled changes are assumed to be + "authoritative". The head of the new changes is used as the first + parent, with local changes as the second. To switch the merge + order, use --switch-parent. + + See 'hg help dates' for a list of formats valid for -d/--date. + ''' + + date = opts.get('date') + if date: + opts['date'] = util.parsedate(date) + + parent, p2 = repo.dirstate.parents() + branch = repo.dirstate.branch() + branchnode = repo.branchtags().get(branch) + if parent != branchnode: + raise util.Abort(_('working dir not at branch tip ' + '(use "hg update" to check out branch tip)')) + + if p2 != nullid: + raise util.Abort(_('outstanding uncommitted merge')) + + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + mod, add, rem, del_ = repo.status()[:4] + + if mod or add or rem: + raise util.Abort(_('outstanding uncommitted changes')) + if del_: + raise util.Abort(_('working directory is missing some files')) + bheads = repo.branchheads(branch) + bheads = [head for head in bheads if len(repo[head].children()) == 0] + if len(bheads) > 1: + raise util.Abort(_('multiple heads in this branch ' + '(use "hg heads ." and "hg merge" to merge)')) + + other = hg.repository(cmdutil.remoteui(repo, opts), + ui.expandpath(source)) + ui.status(_('pulling from %s\n') % + url.hidepassword(ui.expandpath(source))) + revs = None + if opts['rev']: + try: + revs = [other.lookup(rev) for rev in opts['rev']] + except error.CapabilityError: + err = _("Other repository doesn't support revision lookup, " + "so a rev cannot be specified.") + raise util.Abort(err) + + # Are there any changes at all? + modheads = repo.pull(other, heads=revs) + if modheads == 0: + return 0 + + # Is this a simple fast-forward along the current branch? + newheads = repo.branchheads(branch) + newchildren = repo.changelog.nodesbetween([parent], newheads)[2] + if len(newheads) == 1: + if newchildren[0] != parent: + return hg.clean(repo, newchildren[0]) + else: + return + + # Are there more than one additional branch heads? + newchildren = [n for n in newchildren if n != parent] + newparent = parent + if newchildren: + newparent = newchildren[0] + hg.clean(repo, newparent) + newheads = [n for n in newheads if n != newparent] + if len(newheads) > 1: + ui.status(_('not merging with %d other new branch heads ' + '(use "hg heads ." and "hg merge" to merge them)\n') % + (len(newheads) - 1)) + return + + # Otherwise, let's merge. + err = False + if newheads: + # By default, we consider the repository we're pulling + # *from* as authoritative, so we merge our changes into + # theirs. + if opts['switch_parent']: + firstparent, secondparent = newparent, newheads[0] + else: + firstparent, secondparent = newheads[0], newparent + ui.status(_('updating to %d:%s\n') % + (repo.changelog.rev(firstparent), + short(firstparent))) + hg.clean(repo, firstparent) + ui.status(_('merging with %d:%s\n') % + (repo.changelog.rev(secondparent), short(secondparent))) + err = hg.merge(repo, secondparent, remind=False) + + if not err: + # we don't translate commit messages + message = (cmdutil.logmessage(opts) or + ('Automated merge with %s' % + url.removeauth(other.url()))) + editor = cmdutil.commiteditor + if opts.get('force_editor') or opts.get('edit'): + editor = cmdutil.commitforceeditor + n = repo.commit(message, opts['user'], opts['date'], editor=editor) + ui.status(_('new changeset %d:%s merges remote changes ' + 'with local\n') % (repo.changelog.rev(n), + short(n))) + + finally: + release(lock, wlock) + +cmdtable = { + 'fetch': + (fetch, + [('r', 'rev', [], _('a specific revision you would like to pull')), + ('e', 'edit', None, _('edit commit message')), + ('', 'force-editor', None, _('edit commit message (DEPRECATED)')), + ('', 'switch-parent', None, _('switch parents when merging')), + ] + commands.commitopts + commands.commitopts2 + commands.remoteopts, + _('hg fetch [SOURCE]')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/gpg.py b/plugins/hg4idea/testData/bin/hgext/gpg.py new file mode 100644 index 000000000000..3a7244571cad --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/gpg.py @@ -0,0 +1,286 @@ +# Copyright 2005, 2006 Benoit Boissinot +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''commands to sign and verify changesets''' + +import os, tempfile, binascii +from mercurial import util, commands, match +from mercurial import node as hgnode +from mercurial.i18n import _ + +class gpg(object): + def __init__(self, path, key=None): + self.path = path + self.key = (key and " --local-user \"%s\"" % key) or "" + + def sign(self, data): + gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key) + return util.filter(data, gpgcmd) + + def verify(self, data, sig): + """ returns of the good and bad signatures""" + sigfile = datafile = None + try: + # create temporary files + fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig") + fp = os.fdopen(fd, 'wb') + fp.write(sig) + fp.close() + fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt") + fp = os.fdopen(fd, 'wb') + fp.write(data) + fp.close() + gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify " + "\"%s\" \"%s\"" % (self.path, sigfile, datafile)) + ret = util.filter("", gpgcmd) + finally: + for f in (sigfile, datafile): + try: + if f: + os.unlink(f) + except: + pass + keys = [] + key, fingerprint = None, None + err = "" + for l in ret.splitlines(): + # see DETAILS in the gnupg documentation + # filter the logger output + if not l.startswith("[GNUPG:]"): + continue + l = l[9:] + if l.startswith("ERRSIG"): + err = _("error while verifying signature") + break + elif l.startswith("VALIDSIG"): + # fingerprint of the primary key + fingerprint = l.split()[10] + elif (l.startswith("GOODSIG") or + l.startswith("EXPSIG") or + l.startswith("EXPKEYSIG") or + l.startswith("BADSIG")): + if key is not None: + keys.append(key + [fingerprint]) + key = l.split(" ", 2) + fingerprint = None + if err: + return err, [] + if key is not None: + keys.append(key + [fingerprint]) + return err, keys + +def newgpg(ui, **opts): + """create a new gpg instance""" + gpgpath = ui.config("gpg", "cmd", "gpg") + gpgkey = opts.get('key') + if not gpgkey: + gpgkey = ui.config("gpg", "key", None) + return gpg(gpgpath, gpgkey) + +def sigwalk(repo): + """ + walk over every sigs, yields a couple + ((node, version, sig), (filename, linenumber)) + """ + def parsefile(fileiter, context): + ln = 1 + for l in fileiter: + if not l: + continue + yield (l.split(" ", 2), (context, ln)) + ln += 1 + + # read the heads + fl = repo.file(".hgsigs") + for r in reversed(fl.heads()): + fn = ".hgsigs|%s" % hgnode.short(r) + for item in parsefile(fl.read(r).splitlines(), fn): + yield item + try: + # read local signatures + fn = "localsigs" + for item in parsefile(repo.opener(fn), fn): + yield item + except IOError: + pass + +def getkeys(ui, repo, mygpg, sigdata, context): + """get the keys who signed a data""" + fn, ln = context + node, version, sig = sigdata + prefix = "%s:%d" % (fn, ln) + node = hgnode.bin(node) + + data = node2txt(repo, node, version) + sig = binascii.a2b_base64(sig) + err, keys = mygpg.verify(data, sig) + if err: + ui.warn("%s:%d %s\n" % (fn, ln , err)) + return None + + validkeys = [] + # warn for expired key and/or sigs + for key in keys: + if key[0] == "BADSIG": + ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2])) + continue + if key[0] == "EXPSIG": + ui.write(_("%s Note: Signature has expired" + " (signed by: \"%s\")\n") % (prefix, key[2])) + elif key[0] == "EXPKEYSIG": + ui.write(_("%s Note: This key has expired" + " (signed by: \"%s\")\n") % (prefix, key[2])) + validkeys.append((key[1], key[2], key[3])) + return validkeys + +def sigs(ui, repo): + """list signed changesets""" + mygpg = newgpg(ui) + revs = {} + + for data, context in sigwalk(repo): + node, version, sig = data + fn, ln = context + try: + n = repo.lookup(node) + except KeyError: + ui.warn(_("%s:%d node does not exist\n") % (fn, ln)) + continue + r = repo.changelog.rev(n) + keys = getkeys(ui, repo, mygpg, data, context) + if not keys: + continue + revs.setdefault(r, []) + revs[r].extend(keys) + for rev in sorted(revs, reverse=True): + for k in revs[rev]: + r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev))) + ui.write("%-30s %s\n" % (keystr(ui, k), r)) + +def check(ui, repo, rev): + """verify all the signatures there may be for a particular revision""" + mygpg = newgpg(ui) + rev = repo.lookup(rev) + hexrev = hgnode.hex(rev) + keys = [] + + for data, context in sigwalk(repo): + node, version, sig = data + if node == hexrev: + k = getkeys(ui, repo, mygpg, data, context) + if k: + keys.extend(k) + + if not keys: + ui.write(_("No valid signature for %s\n") % hgnode.short(rev)) + return + + # print summary + ui.write("%s is signed by:\n" % hgnode.short(rev)) + for key in keys: + ui.write(" %s\n" % keystr(ui, key)) + +def keystr(ui, key): + """associate a string to a key (username, comment)""" + keyid, user, fingerprint = key + comment = ui.config("gpg", fingerprint, None) + if comment: + return "%s (%s)" % (user, comment) + else: + return user + +def sign(ui, repo, *revs, **opts): + """add a signature for the current or given revision + + If no revision is given, the parent of the working directory is used, + or tip if no revision is checked out. + + See 'hg help dates' for a list of formats valid for -d/--date. + """ + + mygpg = newgpg(ui, **opts) + sigver = "0" + sigmessage = "" + + date = opts.get('date') + if date: + opts['date'] = util.parsedate(date) + + if revs: + nodes = [repo.lookup(n) for n in revs] + else: + nodes = [node for node in repo.dirstate.parents() + if node != hgnode.nullid] + if len(nodes) > 1: + raise util.Abort(_('uncommitted merge - please provide a ' + 'specific revision')) + if not nodes: + nodes = [repo.changelog.tip()] + + for n in nodes: + hexnode = hgnode.hex(n) + ui.write(_("Signing %d:%s\n") % (repo.changelog.rev(n), + hgnode.short(n))) + # build data + data = node2txt(repo, n, sigver) + sig = mygpg.sign(data) + if not sig: + raise util.Abort(_("Error while signing")) + sig = binascii.b2a_base64(sig) + sig = sig.replace("\n", "") + sigmessage += "%s %s %s\n" % (hexnode, sigver, sig) + + # write it + if opts['local']: + repo.opener("localsigs", "ab").write(sigmessage) + return + + msigs = match.exact(repo.root, '', ['.hgsigs']) + s = repo.status(match=msigs, unknown=True, ignored=True)[:6] + if util.any(s) and not opts["force"]: + raise util.Abort(_("working copy of .hgsigs is changed " + "(please commit .hgsigs manually " + "or use --force)")) + + repo.wfile(".hgsigs", "ab").write(sigmessage) + + if '.hgsigs' not in repo.dirstate: + repo.add([".hgsigs"]) + + if opts["no_commit"]: + return + + message = opts['message'] + if not message: + # we don't translate commit messages + message = "\n".join(["Added signature for changeset %s" + % hgnode.short(n) + for n in nodes]) + try: + repo.commit(message, opts['user'], opts['date'], match=msigs) + except ValueError, inst: + raise util.Abort(str(inst)) + +def node2txt(repo, node, ver): + """map a manifest into some text""" + if ver == "0": + return "%s\n" % hgnode.hex(node) + else: + raise util.Abort(_("unknown signature version")) + +cmdtable = { + "sign": + (sign, + [('l', 'local', None, _('make the signature local')), + ('f', 'force', None, _('sign even if the sigfile is modified')), + ('', 'no-commit', None, _('do not commit the sigfile after signing')), + ('k', 'key', '', _('the key id to sign with')), + ('m', 'message', '', _('commit message')), + ] + commands.commitopts2, + _('hg sign [OPTION]... [REVISION]...')), + "sigcheck": (check, [], _('hg sigcheck REVISION')), + "sigs": (sigs, [], _('hg sigs')), +} + diff --git a/plugins/hg4idea/testData/bin/hgext/graphlog.py b/plugins/hg4idea/testData/bin/hgext/graphlog.py new file mode 100644 index 000000000000..5229cad1ad78 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/graphlog.py @@ -0,0 +1,377 @@ +# ASCII graph log extension for Mercurial +# +# Copyright 2007 Joel Rosdahl +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to view revision graphs from a shell + +This extension adds a --graph option to the incoming, outgoing and log +commands. When this options is given, an ASCII representation of the +revision graph is also shown. +''' + +import os +from mercurial.cmdutil import revrange, show_changeset +from mercurial.commands import templateopts +from mercurial.i18n import _ +from mercurial.node import nullrev +from mercurial import bundlerepo, changegroup, cmdutil, commands, extensions +from mercurial import hg, url, util, graphmod + +ASCIIDATA = 'ASC' + +def asciiedges(seen, rev, parents): + """adds edge info to changelog DAG walk suitable for ascii()""" + if rev not in seen: + seen.append(rev) + nodeidx = seen.index(rev) + + knownparents = [] + newparents = [] + for parent in parents: + if parent in seen: + knownparents.append(parent) + else: + newparents.append(parent) + + ncols = len(seen) + seen[nodeidx:nodeidx + 1] = newparents + edges = [(nodeidx, seen.index(p)) for p in knownparents] + + if len(newparents) > 0: + edges.append((nodeidx, nodeidx)) + if len(newparents) > 1: + edges.append((nodeidx, nodeidx + 1)) + + nmorecols = len(seen) - ncols + return nodeidx, edges, ncols, nmorecols + +def fix_long_right_edges(edges): + for (i, (start, end)) in enumerate(edges): + if end > start: + edges[i] = (start, end + 1) + +def get_nodeline_edges_tail( + node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail): + if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0: + # Still going in the same non-vertical direction. + if n_columns_diff == -1: + start = max(node_index + 1, p_node_index) + tail = ["|", " "] * (start - node_index - 1) + tail.extend(["/", " "] * (n_columns - start)) + return tail + else: + return ["\\", " "] * (n_columns - node_index - 1) + else: + return ["|", " "] * (n_columns - node_index - 1) + +def draw_edges(edges, nodeline, interline): + for (start, end) in edges: + if start == end + 1: + interline[2 * end + 1] = "/" + elif start == end - 1: + interline[2 * start + 1] = "\\" + elif start == end: + interline[2 * start] = "|" + else: + nodeline[2 * end] = "+" + if start > end: + (start, end) = (end, start) + for i in range(2 * start + 1, 2 * end): + if nodeline[i] != "+": + nodeline[i] = "-" + +def get_padding_line(ni, n_columns, edges): + line = [] + line.extend(["|", " "] * ni) + if (ni, ni - 1) in edges or (ni, ni) in edges: + # (ni, ni - 1) (ni, ni) + # | | | | | | | | + # +---o | | o---+ + # | | c | | c | | + # | |/ / | |/ / + # | | | | | | + c = "|" + else: + c = " " + line.extend([c, " "]) + line.extend(["|", " "] * (n_columns - ni - 1)) + return line + +def asciistate(): + """returns the initial value for the "state" argument to ascii()""" + return [0, 0] + +def ascii(ui, state, type, char, text, coldata): + """prints an ASCII graph of the DAG + + takes the following arguments (one call per node in the graph): + + - ui to write to + - Somewhere to keep the needed state in (init to asciistate()) + - Column of the current node in the set of ongoing edges. + - Type indicator of node data == ASCIIDATA. + - Payload: (char, lines): + - Character to use as node's symbol. + - List of lines to display as the node's text. + - Edges; a list of (col, next_col) indicating the edges between + the current node and its parents. + - Number of columns (ongoing edges) in the current revision. + - The difference between the number of columns (ongoing edges) + in the next revision and the number of columns (ongoing edges) + in the current revision. That is: -1 means one column removed; + 0 means no columns added or removed; 1 means one column added. + """ + + idx, edges, ncols, coldiff = coldata + assert -2 < coldiff < 2 + if coldiff == -1: + # Transform + # + # | | | | | | + # o | | into o---+ + # |X / |/ / + # | | | | + fix_long_right_edges(edges) + + # add_padding_line says whether to rewrite + # + # | | | | | | | | + # | o---+ into | o---+ + # | / / | | | # <--- padding line + # o | | | / / + # o | | + add_padding_line = (len(text) > 2 and coldiff == -1 and + [x for (x, y) in edges if x + 1 < y]) + + # fix_nodeline_tail says whether to rewrite + # + # | | o | | | | o | | + # | | |/ / | | |/ / + # | o | | into | o / / # <--- fixed nodeline tail + # | |/ / | |/ / + # o | | o | | + fix_nodeline_tail = len(text) <= 2 and not add_padding_line + + # nodeline is the line containing the node character (typically o) + nodeline = ["|", " "] * idx + nodeline.extend([char, " "]) + + nodeline.extend( + get_nodeline_edges_tail(idx, state[1], ncols, coldiff, + state[0], fix_nodeline_tail)) + + # shift_interline is the line containing the non-vertical + # edges between this entry and the next + shift_interline = ["|", " "] * idx + if coldiff == -1: + n_spaces = 1 + edge_ch = "/" + elif coldiff == 0: + n_spaces = 2 + edge_ch = "|" + else: + n_spaces = 3 + edge_ch = "\\" + shift_interline.extend(n_spaces * [" "]) + shift_interline.extend([edge_ch, " "] * (ncols - idx - 1)) + + # draw edges from the current node to its parents + draw_edges(edges, nodeline, shift_interline) + + # lines is the list of all graph lines to print + lines = [nodeline] + if add_padding_line: + lines.append(get_padding_line(idx, ncols, edges)) + lines.append(shift_interline) + + # make sure that there are as many graph lines as there are + # log strings + while len(text) < len(lines): + text.append("") + if len(lines) < len(text): + extra_interline = ["|", " "] * (ncols + coldiff) + while len(lines) < len(text): + lines.append(extra_interline) + + # print lines + indentation_level = max(ncols, ncols + coldiff) + for (line, logstr) in zip(lines, text): + ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr) + ui.write(ln.rstrip() + '\n') + + # ... and start over + state[0] = coldiff + state[1] = idx + +def get_revs(repo, rev_opt): + if rev_opt: + revs = revrange(repo, rev_opt) + return (max(revs), min(revs)) + else: + return (len(repo) - 1, 0) + +def check_unsupported_flags(opts): + for op in ["follow", "follow_first", "date", "copies", "keyword", "remove", + "only_merges", "user", "only_branch", "prune", "newest_first", + "no_merges", "include", "exclude"]: + if op in opts and opts[op]: + raise util.Abort(_("--graph option is incompatible with --%s") + % op.replace("_", "-")) + +def generate(ui, dag, displayer, showparents, edgefn): + seen, state = [], asciistate() + for rev, type, ctx, parents in dag: + char = ctx.node() in showparents and '@' or 'o' + displayer.show(ctx) + lines = displayer.hunk.pop(rev).split('\n')[:-1] + ascii(ui, state, type, char, lines, edgefn(seen, rev, parents)) + +def graphlog(ui, repo, path=None, **opts): + """show revision history alongside an ASCII revision graph + + Print a revision history alongside a revision graph drawn with + ASCII characters. + + Nodes printed as an @ character are parents of the working + directory. + """ + + check_unsupported_flags(opts) + limit = cmdutil.loglimit(opts) + start, stop = get_revs(repo, opts["rev"]) + if start == nullrev: + return + + if path: + path = util.canonpath(repo.root, os.getcwd(), path) + if path: # could be reset in canonpath + revdag = graphmod.filerevs(repo, path, start, stop, limit) + else: + if limit is not None: + stop = max(stop, start - limit + 1) + revdag = graphmod.revisions(repo, start, stop) + + displayer = show_changeset(ui, repo, opts, buffered=True) + showparents = [ctx.node() for ctx in repo[None].parents()] + generate(ui, revdag, displayer, showparents, asciiedges) + +def graphrevs(repo, nodes, opts): + limit = cmdutil.loglimit(opts) + nodes.reverse() + if limit is not None: + nodes = nodes[:limit] + return graphmod.nodes(repo, nodes) + +def goutgoing(ui, repo, dest=None, **opts): + """show the outgoing changesets alongside an ASCII revision graph + + Print the outgoing changesets alongside a revision graph drawn with + ASCII characters. + + Nodes printed as an @ character are parents of the working + directory. + """ + + check_unsupported_flags(opts) + dest = ui.expandpath(dest or 'default-push', dest or 'default') + dest, branches = hg.parseurl(dest, opts.get('branch')) + revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev')) + other = hg.repository(cmdutil.remoteui(ui, opts), dest) + if revs: + revs = [repo.lookup(rev) for rev in revs] + ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + o = repo.findoutgoing(other, force=opts.get('force')) + if not o: + ui.status(_("no changes found\n")) + return + + o = repo.changelog.nodesbetween(o, revs)[0] + revdag = graphrevs(repo, o, opts) + displayer = show_changeset(ui, repo, opts, buffered=True) + showparents = [ctx.node() for ctx in repo[None].parents()] + generate(ui, revdag, displayer, showparents, asciiedges) + +def gincoming(ui, repo, source="default", **opts): + """show the incoming changesets alongside an ASCII revision graph + + Print the incoming changesets alongside a revision graph drawn with + ASCII characters. + + Nodes printed as an @ character are parents of the working + directory. + """ + + check_unsupported_flags(opts) + source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch')) + other = hg.repository(cmdutil.remoteui(repo, opts), source) + revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) + ui.status(_('comparing with %s\n') % url.hidepassword(source)) + if revs: + revs = [other.lookup(rev) for rev in revs] + incoming = repo.findincoming(other, heads=revs, force=opts["force"]) + if not incoming: + try: + os.unlink(opts["bundle"]) + except: + pass + ui.status(_("no changes found\n")) + return + + cleanup = None + try: + + fname = opts["bundle"] + if fname or not other.local(): + # create a bundle (uncompressed if other repo is not local) + if revs is None: + cg = other.changegroup(incoming, "incoming") + else: + cg = other.changegroupsubset(incoming, revs, 'incoming') + bundletype = other.local() and "HG10BZ" or "HG10UN" + fname = cleanup = changegroup.writebundle(cg, fname, bundletype) + # keep written bundle? + if opts["bundle"]: + cleanup = None + if not other.local(): + # use the created uncompressed bundlerepo + other = bundlerepo.bundlerepository(ui, repo.root, fname) + + chlist = other.changelog.nodesbetween(incoming, revs)[0] + revdag = graphrevs(other, chlist, opts) + displayer = show_changeset(ui, other, opts, buffered=True) + showparents = [ctx.node() for ctx in repo[None].parents()] + generate(ui, revdag, displayer, showparents, asciiedges) + + finally: + if hasattr(other, 'close'): + other.close() + if cleanup: + os.unlink(cleanup) + +def uisetup(ui): + '''Initialize the extension.''' + _wrapcmd(ui, 'log', commands.table, graphlog) + _wrapcmd(ui, 'incoming', commands.table, gincoming) + _wrapcmd(ui, 'outgoing', commands.table, goutgoing) + +def _wrapcmd(ui, cmd, table, wrapfn): + '''wrap the command''' + def graph(orig, *args, **kwargs): + if kwargs['graph']: + return wrapfn(*args, **kwargs) + return orig(*args, **kwargs) + entry = extensions.wrapcommand(table, cmd, graph) + entry[1].append(('G', 'graph', None, _("show the revision DAG"))) + +cmdtable = { + "glog": + (graphlog, + [('l', 'limit', '', _('limit number of changes displayed')), + ('p', 'patch', False, _('show patch')), + ('r', 'rev', [], _('show the specified revision or range')), + ] + templateopts, + _('hg glog [OPTION]... [FILE]')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/hgcia.py b/plugins/hg4idea/testData/bin/hgext/hgcia.py new file mode 100644 index 000000000000..caf40fb9003b --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/hgcia.py @@ -0,0 +1,251 @@ +# Copyright (C) 2007-8 Brendan Cully +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +"""hooks for integrating with the CIA.vc notification service + +This is meant to be run as a changegroup or incoming hook. To +configure it, set the following options in your hgrc:: + + [cia] + # your registered CIA user name + user = foo + # the name of the project in CIA + project = foo + # the module (subproject) (optional) + #module = foo + # Append a diffstat to the log message (optional) + #diffstat = False + # Template to use for log messages (optional) + #template = {desc}\\n{baseurl}/rev/{node}-- {diffstat} + # Style to use (optional) + #style = foo + # The URL of the CIA notification service (optional) + # You can use mailto: URLs to send by email, eg + # mailto:cia@cia.vc + # Make sure to set email.from if you do this. + #url = http://cia.vc/ + # print message instead of sending it (optional) + #test = False + + [hooks] + # one of these: + changegroup.cia = python:hgcia.hook + #incoming.cia = python:hgcia.hook + + [web] + # If you want hyperlinks (optional) + baseurl = http://server/path/to/repo +""" + +from mercurial.i18n import _ +from mercurial.node import * +from mercurial import cmdutil, patch, templater, util, mail +import email.Parser + +import xmlrpclib +from xml.sax import saxutils + +socket_timeout = 30 # seconds +try: + # set a timeout for the socket so you don't have to wait so looooong + # when cia.vc is having problems. requires python >= 2.3: + import socket + socket.setdefaulttimeout(socket_timeout) +except: + pass + +HGCIA_VERSION = '0.1' +HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia' + + +class ciamsg(object): + """ A CIA message """ + def __init__(self, cia, ctx): + self.cia = cia + self.ctx = ctx + self.url = self.cia.url + + def fileelem(self, path, uri, action): + if uri: + uri = ' uri=%s' % saxutils.quoteattr(uri) + return '%s' % ( + uri, saxutils.quoteattr(action), saxutils.escape(path)) + + def fileelems(self): + n = self.ctx.node() + f = self.cia.repo.status(self.ctx.parents()[0].node(), n) + url = self.url or '' + elems = [] + for path in f[0]: + uri = '%s/diff/%s/%s' % (url, short(n), path) + elems.append(self.fileelem(path, url and uri, 'modify')) + for path in f[1]: + # TODO: copy/rename ? + uri = '%s/file/%s/%s' % (url, short(n), path) + elems.append(self.fileelem(path, url and uri, 'add')) + for path in f[2]: + elems.append(self.fileelem(path, '', 'remove')) + + return '\n'.join(elems) + + def sourceelem(self, project, module=None, branch=None): + msg = ['', '%s' % saxutils.escape(project)] + if module: + msg.append('%s' % saxutils.escape(module)) + if branch: + msg.append('%s' % saxutils.escape(branch)) + msg.append('') + + return '\n'.join(msg) + + def diffstat(self): + class patchbuf(object): + def __init__(self): + self.lines = [] + # diffstat is stupid + self.name = 'cia' + def write(self, data): + self.lines.append(data) + def close(self): + pass + + n = self.ctx.node() + pbuf = patchbuf() + patch.export(self.cia.repo, [n], fp=pbuf) + return patch.diffstat(pbuf.lines) or '' + + def logmsg(self): + diffstat = self.cia.diffstat and self.diffstat() or '' + self.cia.ui.pushbuffer() + self.cia.templater.show(self.ctx, changes=self.ctx.changeset(), + url=self.cia.url, diffstat=diffstat) + return self.cia.ui.popbuffer() + + def xml(self): + n = short(self.ctx.node()) + src = self.sourceelem(self.cia.project, module=self.cia.module, + branch=self.ctx.branch()) + # unix timestamp + dt = self.ctx.date() + timestamp = dt[0] + + author = saxutils.escape(self.ctx.user()) + rev = '%d:%s' % (self.ctx.rev(), n) + log = saxutils.escape(self.logmsg()) + + url = self.url and '%s/rev/%s' % (saxutils.escape(self.url), + n) or '' + + msg = """ + + + Mercurial (hgcia) + %s + %s + %s + + %s + + + %s + %s + %s + %s + %s + + + %d + +""" % \ + (HGCIA_VERSION, saxutils.escape(HGCIA_URL), + saxutils.escape(self.cia.user), src, author, rev, log, url, + self.fileelems(), timestamp) + + return msg + + +class hgcia(object): + """ CIA notification class """ + + deftemplate = '{desc}' + dstemplate = '{desc}\n-- \n{diffstat}' + + def __init__(self, ui, repo): + self.ui = ui + self.repo = repo + + self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc') + self.user = self.ui.config('cia', 'user') + self.project = self.ui.config('cia', 'project') + self.module = self.ui.config('cia', 'module') + self.diffstat = self.ui.configbool('cia', 'diffstat') + self.emailfrom = self.ui.config('email', 'from') + self.dryrun = self.ui.configbool('cia', 'test') + self.url = self.ui.config('web', 'baseurl') + + style = self.ui.config('cia', 'style') + template = self.ui.config('cia', 'template') + if not template: + template = self.diffstat and self.dstemplate or self.deftemplate + template = templater.parsestring(template, quoted=False) + t = cmdutil.changeset_templater(self.ui, self.repo, False, None, + style, False) + t.use_template(template) + self.templater = t + + def sendrpc(self, msg): + srv = xmlrpclib.Server(self.ciaurl) + res = srv.hub.deliver(msg) + if res is not True: + raise util.Abort(_('%s returned an error: %s') % + (self.ciaurl, res)) + + def sendemail(self, address, data): + p = email.Parser.Parser() + msg = p.parsestr(data) + msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2") + msg['To'] = address + msg['From'] = self.emailfrom + msg['Subject'] = 'DeliverXML' + msg['Content-type'] = 'text/xml' + msgtext = msg.as_string() + + self.ui.status(_('hgcia: sending update to %s\n') % address) + mail.sendmail(self.ui, util.email(self.emailfrom), + [address], msgtext) + + +def hook(ui, repo, hooktype, node=None, url=None, **kwargs): + """ send CIA notification """ + def sendmsg(cia, ctx): + msg = ciamsg(cia, ctx).xml() + if cia.dryrun: + ui.write(msg) + elif cia.ciaurl.startswith('mailto:'): + if not cia.emailfrom: + raise util.Abort(_('email.from must be defined when ' + 'sending by email')) + cia.sendemail(cia.ciaurl[7:], msg) + else: + cia.sendrpc(msg) + + n = bin(node) + cia = hgcia(ui, repo) + if not cia.user: + ui.debug('cia: no user specified') + return + if not cia.project: + ui.debug('cia: no project specified') + return + if hooktype == 'changegroup': + start = repo.changelog.rev(n) + end = len(repo.changelog) + for rev in xrange(start, end): + n = repo.changelog.node(rev) + ctx = repo.changectx(n) + sendmsg(cia, ctx) + else: + ctx = repo.changectx(n) + sendmsg(cia, ctx) diff --git a/plugins/hg4idea/testData/bin/hgext/hgk.py b/plugins/hg4idea/testData/bin/hgext/hgk.py new file mode 100644 index 000000000000..5a16a2f03c60 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/hgk.py @@ -0,0 +1,347 @@ +# Minimal support for git commands on an hg repository +# +# Copyright 2005, 2006 Chris Mason +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''browse the repository in a graphical way + +The hgk extension allows browsing the history of a repository in a +graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not +distributed with Mercurial.) + +hgk consists of two parts: a Tcl script that does the displaying and +querying of information, and an extension to Mercurial named hgk.py, +which provides hooks for hgk to get information. hgk can be found in +the contrib directory, and the extension is shipped in the hgext +repository, and needs to be enabled. + +The hg view command will launch the hgk Tcl script. For this command +to work, hgk must be in your search path. Alternately, you can specify +the path to hgk in your .hgrc file:: + + [hgk] + path=/location/of/hgk + +hgk can make use of the extdiff extension to visualize revisions. +Assuming you had already configured extdiff vdiff command, just add:: + + [hgk] + vdiff=vdiff + +Revisions context menu will now display additional entries to fire +vdiff on hovered and selected revisions. +''' + +import os +from mercurial import commands, util, patch, revlog, cmdutil +from mercurial.node import nullid, nullrev, short +from mercurial.i18n import _ + +def difftree(ui, repo, node1=None, node2=None, *files, **opts): + """diff trees from two commits""" + def __difftree(repo, node1, node2, files=[]): + assert node2 is not None + mmap = repo[node1].manifest() + mmap2 = repo[node2].manifest() + m = cmdutil.match(repo, files) + modified, added, removed = repo.status(node1, node2, m)[:3] + empty = short(nullid) + + for f in modified: + # TODO get file permissions + ui.write(":100664 100664 %s %s M\t%s\t%s\n" % + (short(mmap[f]), short(mmap2[f]), f, f)) + for f in added: + ui.write(":000000 100664 %s %s N\t%s\t%s\n" % + (empty, short(mmap2[f]), f, f)) + for f in removed: + ui.write(":100664 000000 %s %s D\t%s\t%s\n" % + (short(mmap[f]), empty, f, f)) + ## + + while True: + if opts['stdin']: + try: + line = raw_input().split(' ') + node1 = line[0] + if len(line) > 1: + node2 = line[1] + else: + node2 = None + except EOFError: + break + node1 = repo.lookup(node1) + if node2: + node2 = repo.lookup(node2) + else: + node2 = node1 + node1 = repo.changelog.parents(node1)[0] + if opts['patch']: + if opts['pretty']: + catcommit(ui, repo, node2, "") + m = cmdutil.match(repo, files) + chunks = patch.diff(repo, node1, node2, match=m, + opts=patch.diffopts(ui, {'git': True})) + for chunk in chunks: + ui.write(chunk) + else: + __difftree(repo, node1, node2, files=files) + if not opts['stdin']: + break + +def catcommit(ui, repo, n, prefix, ctx=None): + nlprefix = '\n' + prefix + if ctx is None: + ctx = repo[n] + ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ?? + for p in ctx.parents(): + ui.write("parent %s\n" % p) + + date = ctx.date() + description = ctx.description().replace("\0", "") + lines = description.splitlines() + if lines and lines[-1].startswith('committer:'): + committer = lines[-1].split(': ')[1].rstrip() + else: + committer = ctx.user() + + ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1])) + ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1])) + ui.write("revision %d\n" % ctx.rev()) + ui.write("branch %s\n\n" % ctx.branch()) + + if prefix != "": + ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip())) + else: + ui.write(description + "\n") + if prefix: + ui.write('\0') + +def base(ui, repo, node1, node2): + """output common ancestor information""" + node1 = repo.lookup(node1) + node2 = repo.lookup(node2) + n = repo.changelog.ancestor(node1, node2) + ui.write(short(n) + "\n") + +def catfile(ui, repo, type=None, r=None, **opts): + """cat a specific revision""" + # in stdin mode, every line except the commit is prefixed with two + # spaces. This way the our caller can find the commit without magic + # strings + # + prefix = "" + if opts['stdin']: + try: + (type, r) = raw_input().split(' ') + prefix = " " + except EOFError: + return + + else: + if not type or not r: + ui.warn(_("cat-file: type or revision not supplied\n")) + commands.help_(ui, 'cat-file') + + while r: + if type != "commit": + ui.warn(_("aborting hg cat-file only understands commits\n")) + return 1 + n = repo.lookup(r) + catcommit(ui, repo, n, prefix) + if opts['stdin']: + try: + (type, r) = raw_input().split(' ') + except EOFError: + break + else: + break + +# git rev-tree is a confusing thing. You can supply a number of +# commit sha1s on the command line, and it walks the commit history +# telling you which commits are reachable from the supplied ones via +# a bitmask based on arg position. +# you can specify a commit to stop at by starting the sha1 with ^ +def revtree(ui, args, repo, full="tree", maxnr=0, parents=False): + def chlogwalk(): + count = len(repo) + i = count + l = [0] * 100 + chunk = 100 + while True: + if chunk > i: + chunk = i + i = 0 + else: + i -= chunk + + for x in xrange(chunk): + if i + x >= count: + l[chunk - x:] = [0] * (chunk - x) + break + if full != None: + l[x] = repo[i + x] + l[x].changeset() # force reading + else: + l[x] = 1 + for x in xrange(chunk - 1, -1, -1): + if l[x] != 0: + yield (i + x, full != None and l[x] or None) + if i == 0: + break + + # calculate and return the reachability bitmask for sha + def is_reachable(ar, reachable, sha): + if len(ar) == 0: + return 1 + mask = 0 + for i in xrange(len(ar)): + if sha in reachable[i]: + mask |= 1 << i + + return mask + + reachable = [] + stop_sha1 = [] + want_sha1 = [] + count = 0 + + # figure out which commits they are asking for and which ones they + # want us to stop on + for i, arg in enumerate(args): + if arg.startswith('^'): + s = repo.lookup(arg[1:]) + stop_sha1.append(s) + want_sha1.append(s) + elif arg != 'HEAD': + want_sha1.append(repo.lookup(arg)) + + # calculate the graph for the supplied commits + for i, n in enumerate(want_sha1): + reachable.append(set()) + visit = [n] + reachable[i].add(n) + while visit: + n = visit.pop(0) + if n in stop_sha1: + continue + for p in repo.changelog.parents(n): + if p not in reachable[i]: + reachable[i].add(p) + visit.append(p) + if p in stop_sha1: + continue + + # walk the repository looking for commits that are in our + # reachability graph + for i, ctx in chlogwalk(): + n = repo.changelog.node(i) + mask = is_reachable(want_sha1, reachable, n) + if mask: + parentstr = "" + if parents: + pp = repo.changelog.parents(n) + if pp[0] != nullid: + parentstr += " " + short(pp[0]) + if pp[1] != nullid: + parentstr += " " + short(pp[1]) + if not full: + ui.write("%s%s\n" % (short(n), parentstr)) + elif full == "commit": + ui.write("%s%s\n" % (short(n), parentstr)) + catcommit(ui, repo, n, ' ', ctx) + else: + (p1, p2) = repo.changelog.parents(n) + (h, h1, h2) = map(short, (n, p1, p2)) + (i1, i2) = map(repo.changelog.rev, (p1, p2)) + + date = ctx.date()[0] + ui.write("%s %s:%s" % (date, h, mask)) + mask = is_reachable(want_sha1, reachable, p1) + if i1 != nullrev and mask > 0: + ui.write("%s:%s " % (h1, mask)), + mask = is_reachable(want_sha1, reachable, p2) + if i2 != nullrev and mask > 0: + ui.write("%s:%s " % (h2, mask)) + ui.write("\n") + if maxnr and count >= maxnr: + break + count += 1 + +def revparse(ui, repo, *revs, **opts): + """parse given revisions""" + def revstr(rev): + if rev == 'HEAD': + rev = 'tip' + return revlog.hex(repo.lookup(rev)) + + for r in revs: + revrange = r.split(':', 1) + ui.write('%s\n' % revstr(revrange[0])) + if len(revrange) == 2: + ui.write('^%s\n' % revstr(revrange[1])) + +# git rev-list tries to order things by date, and has the ability to stop +# at a given commit without walking the whole repo. TODO add the stop +# parameter +def revlist(ui, repo, *revs, **opts): + """print revisions""" + if opts['header']: + full = "commit" + else: + full = None + copy = [x for x in revs] + revtree(ui, copy, repo, full, opts['max_count'], opts['parents']) + +def config(ui, repo, **opts): + """print extension options""" + def writeopt(name, value): + ui.write('k=%s\nv=%s\n' % (name, value)) + + writeopt('vdiff', ui.config('hgk', 'vdiff', '')) + + +def view(ui, repo, *etc, **opts): + "start interactive history viewer" + os.chdir(repo.root) + optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v]) + cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc)) + ui.debug("running %s\n" % cmd) + util.system(cmd) + +cmdtable = { + "^view": + (view, + [('l', 'limit', '', _('limit number of changes displayed'))], + _('hg view [-l LIMIT] [REVRANGE]')), + "debug-diff-tree": + (difftree, + [('p', 'patch', None, _('generate patch')), + ('r', 'recursive', None, _('recursive')), + ('P', 'pretty', None, _('pretty')), + ('s', 'stdin', None, _('stdin')), + ('C', 'copy', None, _('detect copies')), + ('S', 'search', "", _('search'))], + _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')), + "debug-cat-file": + (catfile, + [('s', 'stdin', None, _('stdin'))], + _('hg debug-cat-file [OPTION]... TYPE FILE')), + "debug-config": + (config, [], _('hg debug-config')), + "debug-merge-base": + (base, [], _('hg debug-merge-base REV REV')), + "debug-rev-parse": + (revparse, + [('', 'default', '', _('ignored'))], + _('hg debug-rev-parse REV')), + "debug-rev-list": + (revlist, + [('H', 'header', None, _('header')), + ('t', 'topo-order', None, _('topo-order')), + ('p', 'parents', None, _('parents')), + ('n', 'max-count', 0, _('max-count'))], + _('hg debug-rev-list [OPTION]... REV...')), +} diff --git a/plugins/hg4idea/testData/bin/hgext/highlight/__init__.py b/plugins/hg4idea/testData/bin/hgext/highlight/__init__.py new file mode 100644 index 000000000000..55e3c180f008 --- /dev/null +++ b/plugins/hg4idea/testData/bin/hgext/highlight/__init__.py @@ -0,0 +1,61 @@ +# highlight - syntax highlighting in hgweb, based on Pygments +# +# Copyright 2008, 2009 Patrick Mezard and others +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +# +# The original module was split in an interface and an implementation +# file to defer pygments loading and speedup extension setup. + +"""syntax highlighting for hgweb (requires Pygments) + +It depends on the Pygments syntax highlighting library: +http://pygments.org/ + +There is a single configuration option:: + + [web] + pygments_style =