| // 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.provider; |
| |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.vcs.*; |
| import com.intellij.openapi.vcs.changes.*; |
| import com.intellij.openapi.vfs.VfsUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.JBColor; |
| import com.intellij.util.Function; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.vcsUtil.VcsUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.zmlx.hg4idea.*; |
| import org.zmlx.hg4idea.command.HgResolveCommand; |
| import org.zmlx.hg4idea.command.HgResolveStatusEnum; |
| import org.zmlx.hg4idea.command.HgStatusCommand; |
| import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand; |
| import org.zmlx.hg4idea.repo.HgRepository; |
| import org.zmlx.hg4idea.util.HgUtil; |
| |
| import java.io.File; |
| import java.util.*; |
| |
| public class HgChangeProvider implements ChangeProvider { |
| |
| private final Project myProject; |
| private final VcsKey myVcsKey; |
| |
| public static final FileStatus COPIED = FileStatusFactory.getInstance().createFileStatus("COPIED", "Copied", FileStatus.COLOR_ADDED); |
| public static final FileStatus RENAMED = FileStatusFactory.getInstance().createFileStatus("RENAMED", "Renamed", |
| JBColor.CYAN.darker().darker()); |
| |
| private static final EnumMap<HgFileStatusEnum, HgChangeProcessor> PROCESSORS = |
| new EnumMap<HgFileStatusEnum, HgChangeProcessor>(HgFileStatusEnum.class); |
| |
| static { |
| PROCESSORS.put(HgFileStatusEnum.ADDED, HgChangeProcessor.ADDED); |
| PROCESSORS.put(HgFileStatusEnum.DELETED, HgChangeProcessor.DELETED); |
| PROCESSORS.put(HgFileStatusEnum.IGNORED, HgChangeProcessor.IGNORED); |
| PROCESSORS.put(HgFileStatusEnum.MISSING, HgChangeProcessor.MISSING); |
| PROCESSORS.put(HgFileStatusEnum.COPY, HgChangeProcessor.COPIED); |
| PROCESSORS.put(HgFileStatusEnum.MODIFIED, HgChangeProcessor.MODIFIED); |
| PROCESSORS.put(HgFileStatusEnum.UNMODIFIED, HgChangeProcessor.UNMODIFIED); |
| PROCESSORS.put(HgFileStatusEnum.UNVERSIONED, HgChangeProcessor.UNVERSIONED); |
| } |
| |
| public HgChangeProvider(Project project, VcsKey vcsKey) { |
| myProject = project; |
| myVcsKey = vcsKey; |
| } |
| |
| public boolean isModifiedDocumentTrackingRequired() { |
| return true; |
| } |
| |
| public void doCleanup(List<VirtualFile> files) { |
| } |
| |
| public void getChanges(VcsDirtyScope dirtyScope, ChangelistBuilder builder, |
| ProgressIndicator progress, ChangeListManagerGate addGate) throws VcsException { |
| if (myProject.isDisposed()) return; |
| final Collection<HgChange> changes = new HashSet<HgChange>(); |
| changes.addAll(process(builder, dirtyScope.getRecursivelyDirtyDirectories())); |
| changes.addAll(process(builder, dirtyScope.getDirtyFiles())); |
| processUnsavedChanges(builder, dirtyScope.getDirtyFilesNoExpand(), changes); |
| } |
| |
| private Collection<HgChange> process(ChangelistBuilder builder, Collection<FilePath> files) { |
| final Set<HgChange> hgChanges = new HashSet<HgChange>(); |
| for (Map.Entry<VirtualFile, Collection<FilePath>> entry : HgUtil.groupFilePathsByHgRoots(myProject, files).entrySet()) { |
| VirtualFile repo = entry.getKey(); |
| |
| final HgRevisionNumber workingRevision = new HgWorkingCopyRevisionsCommand(myProject).identify(repo).getFirst(); |
| final HgRevisionNumber parentRevision = new HgWorkingCopyRevisionsCommand(myProject).firstParent(repo); |
| final Map<HgFile, HgResolveStatusEnum> list = new HgResolveCommand(myProject).getListSynchronously(repo); |
| |
| hgChanges.addAll(new HgStatusCommand.Builder(true).build(myProject).execute(repo, entry.getValue())); |
| final HgRepository hgRepo = HgUtil.getRepositoryForFile(myProject, repo); |
| if (hgRepo != null && hgRepo.hasSubrepos()) { |
| hgChanges.addAll(ContainerUtil.mapNotNull(hgRepo.getSubrepos(), new Function<HgNameWithHashInfo, HgChange>() { |
| @Override |
| public HgChange fun(HgNameWithHashInfo info) { |
| return findChange(hgRepo, info); |
| } |
| })); |
| } |
| |
| sendChanges(builder, hgChanges, list, workingRevision, parentRevision); |
| } |
| return hgChanges; |
| } |
| |
| @Nullable |
| private HgChange findChange(@NotNull HgRepository hgRepo, @NotNull HgNameWithHashInfo info) { |
| File file = new File(hgRepo.getRoot().getPath(), info.getName()); |
| VirtualFile virtualSubrepoFile = VfsUtil.findFileByIoFile(file, false); |
| HgRepository subrepo = HgUtil.getRepositoryForFile(myProject, virtualSubrepoFile); |
| if (subrepo != null && !info.getHash().asString().equals(subrepo.getCurrentRevision())) { |
| return new HgChange(new HgFile(hgRepo.getRoot(), new FilePathImpl(virtualSubrepoFile)), HgFileStatusEnum.MODIFIED); |
| } |
| return null; |
| } |
| |
| private void sendChanges(ChangelistBuilder builder, Set<HgChange> changes, |
| Map<HgFile, HgResolveStatusEnum> resolveStatus, HgRevisionNumber workingRevision, |
| HgRevisionNumber parentRevision) { |
| for (HgChange change : changes) { |
| HgFile afterFile = change.afterFile(); |
| HgFile beforeFile = change.beforeFile(); |
| HgFileStatusEnum status = change.getStatus(); |
| |
| if (resolveStatus.containsKey(afterFile) |
| && resolveStatus.get(afterFile) == HgResolveStatusEnum.UNRESOLVED) { |
| builder.processChange( |
| new Change( |
| new HgContentRevision(myProject, beforeFile, parentRevision), |
| HgCurrentContentRevision.create(afterFile, workingRevision), |
| FileStatus.MERGED_WITH_CONFLICTS |
| ), myVcsKey); |
| continue; |
| } |
| |
| if (isDeleteOfCopiedFile(change, changes)) { |
| // Don't register the 'delete' change for renamed or moved files; IDEA already handles these |
| // itself. |
| continue; |
| } |
| |
| HgChangeProcessor processor = PROCESSORS.get(status); |
| if (processor != null) { |
| processor.process(myProject, myVcsKey, builder, |
| workingRevision, parentRevision, beforeFile, afterFile); |
| } |
| } |
| } |
| |
| private static boolean isDeleteOfCopiedFile(@NotNull HgChange change, Set<HgChange> changes) { |
| if (change.getStatus().equals(HgFileStatusEnum.DELETED)) { |
| for (HgChange otherChange : changes) { |
| if (otherChange.getStatus().equals(HgFileStatusEnum.COPY) && |
| otherChange.beforeFile().equals(change.afterFile())) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Finds modified but unsaved files in the given list of dirty files and notifies the builder about MODIFIED changes. |
| * Changes contained in <code>alreadyProcessed</code> are skipped - they have already been processed as modified, or else. |
| */ |
| public void processUnsavedChanges(ChangelistBuilder builder, Set<FilePath> dirtyFiles, Collection<HgChange> alreadyProcessed) { |
| // exclude already processed |
| for (HgChange c : alreadyProcessed) { |
| dirtyFiles.remove(c.beforeFile().toFilePath()); |
| dirtyFiles.remove(c.afterFile().toFilePath()); |
| } |
| |
| final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject); |
| final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); |
| for (FilePath filePath : dirtyFiles) { |
| final VirtualFile vf = filePath.getVirtualFile(); |
| if (vf != null && fileDocumentManager.isFileModified(vf)) { |
| final VirtualFile root = vcsManager.getVcsRootFor(vf); |
| if (root != null && HgUtil.isHgRoot(root)) { |
| final HgRevisionNumber beforeRevisionNumber = new HgWorkingCopyRevisionsCommand(myProject).tip(root); |
| final ContentRevision beforeRevision = (beforeRevisionNumber == null ? null : |
| new HgContentRevision(myProject, new HgFile(myProject, vf), beforeRevisionNumber)); |
| builder.processChange(new Change(beforeRevision, CurrentContentRevision.create(filePath), FileStatus.MODIFIED), myVcsKey); |
| } |
| } |
| } |
| } |
| |
| |
| private enum HgChangeProcessor { |
| ADDED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| processChange( |
| null, |
| HgCurrentContentRevision.create(afterFile, currentNumber), |
| FileStatus.ADDED, |
| builder, |
| vcsKey |
| ); |
| } |
| }, |
| |
| DELETED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| processChange( |
| new HgContentRevision(project, beforeFile, parentRevision), |
| null, |
| FileStatus.DELETED, |
| builder, |
| vcsKey |
| ); |
| } |
| }, |
| |
| IGNORED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| builder.processIgnoredFile(VcsUtil.getVirtualFile(afterFile.getFile())); |
| } |
| }, |
| |
| MISSING() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| builder.processLocallyDeletedFile(new LocallyDeletedChange(beforeFile.toFilePath())); |
| } |
| }, |
| |
| COPIED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| if (beforeFile.getFile().exists()) { |
| // The original file exists so this is a duplication of the file. |
| // Don't create the before ContentRevision or IDEA will think |
| // this was a rename. |
| //todo: fix this unexpected status behavior (sometimes added status instead of copied, and copied instead of renamed ) |
| processChange( |
| null, |
| HgCurrentContentRevision.create(afterFile, currentNumber), |
| FileStatus.ADDED, |
| builder, |
| vcsKey |
| ); |
| } else { |
| // The original file does not exists so this is a rename. |
| processChange( |
| new HgContentRevision(project, beforeFile, parentRevision), |
| HgCurrentContentRevision.create(afterFile, currentNumber), |
| HgChangeProvider.RENAMED, |
| builder, |
| vcsKey |
| ); |
| } |
| } |
| }, |
| |
| MODIFIED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| processChange( |
| new HgContentRevision(project, beforeFile, parentRevision), |
| HgCurrentContentRevision.create(afterFile, currentNumber), |
| FileStatus.MODIFIED, |
| builder, |
| vcsKey |
| ); |
| } |
| }, |
| |
| UNMODIFIED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| //DO NOTHING |
| } |
| }, |
| |
| UNVERSIONED() { |
| @Override |
| void process(Project project, VcsKey vcsKey, ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, HgRevisionNumber parentRevision, |
| HgFile beforeFile, HgFile afterFile) { |
| builder.processUnversionedFile(VcsUtil.getVirtualFile(afterFile.getFile())); |
| } |
| }; |
| |
| abstract void process( |
| Project project, |
| VcsKey vcsKey, |
| ChangelistBuilder builder, |
| HgRevisionNumber currentNumber, |
| HgRevisionNumber parentRevision, |
| HgFile beforeFile, |
| HgFile afterFile |
| ); |
| |
| static void processChange(ContentRevision contentRevisionBefore, |
| ContentRevision contentRevisionAfter, FileStatus fileStatus, |
| ChangelistBuilder builder, VcsKey vcsKey) { |
| if (contentRevisionBefore == null && contentRevisionAfter == null) { |
| return; |
| } |
| builder.processChange( |
| new Change(contentRevisionBefore, contentRevisionAfter, fileStatus), |
| vcsKey |
| ); |
| } |
| } |
| } |