| // 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.commit; |
| |
| import com.intellij.dvcs.DvcsCommitAdditionalComponent; |
| import com.intellij.dvcs.push.ui.VcsPushDialog; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.Task; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.vcs.CheckinProjectPanel; |
| import com.intellij.openapi.vcs.FilePath; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vcs.changes.*; |
| import com.intellij.openapi.vcs.checkin.CheckinEnvironment; |
| import com.intellij.openapi.vcs.ui.RefreshableOnComponent; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.util.FunctionUtil; |
| import com.intellij.util.NullableFunction; |
| import com.intellij.util.PairConsumer; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.vcsUtil.VcsUtil; |
| import com.intellij.xml.util.XmlStringUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.zmlx.hg4idea.*; |
| import org.zmlx.hg4idea.action.HgActionUtil; |
| import org.zmlx.hg4idea.command.*; |
| import org.zmlx.hg4idea.execution.HgCommandException; |
| import org.zmlx.hg4idea.execution.HgCommandExecutor; |
| import org.zmlx.hg4idea.execution.HgCommandResult; |
| import org.zmlx.hg4idea.provider.HgCurrentBinaryContentRevision; |
| import org.zmlx.hg4idea.repo.HgRepository; |
| import org.zmlx.hg4idea.util.HgUtil; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.util.*; |
| import java.util.List; |
| |
| public class HgCheckinEnvironment implements CheckinEnvironment { |
| |
| private final Project myProject; |
| private boolean myNextCommitIsPushed; |
| private boolean myNextCommitAmend; // If true, the next commit is amended |
| private boolean myShouldCommitSubrepos; |
| |
| public HgCheckinEnvironment(Project project) { |
| myProject = project; |
| } |
| |
| public RefreshableOnComponent createAdditionalOptionsPanel(CheckinProjectPanel panel, |
| PairConsumer<Object, Object> additionalDataConsumer) { |
| myNextCommitIsPushed = false; |
| return new HgCommitAdditionalComponent(myProject,panel); |
| } |
| |
| public String getDefaultMessageFor(FilePath[] filesToCheckin) { |
| return null; |
| } |
| |
| public String getHelpId() { |
| return null; |
| } |
| |
| public String getCheckinOperationName() { |
| return HgVcsMessages.message("hg4idea.commit"); |
| } |
| |
| @SuppressWarnings({"ThrowableInstanceNeverThrown"}) |
| public List<VcsException> commit(List<Change> changes, |
| String preparedComment, |
| @NotNull NullableFunction<Object, Object> parametersHolder, |
| Set<String> feedback) { |
| List<VcsException> exceptions = new LinkedList<VcsException>(); |
| Map<HgRepository, Set<HgFile>> repositoriesMap = getFilesByRepository(changes); |
| for (Map.Entry<HgRepository, Set<HgFile>> entry : repositoriesMap.entrySet()) { |
| |
| HgRepository repo = entry.getKey(); |
| Set<HgFile> selectedFiles = entry.getValue(); |
| HgCommitCommand command = |
| new HgCommitCommand(myProject, repo.getRoot(), preparedComment, myNextCommitAmend); |
| |
| if (isMergeCommit(repo.getRoot())) { |
| //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<HgFile> changedFilesNotInCommit = getChangedFilesNotInCommit(repo.getRoot(), selectedFiles); |
| boolean partial = !changedFilesNotInCommit.isEmpty(); |
| |
| |
| if (partial) { |
| final StringBuilder filesNotIncludedString = new StringBuilder(); |
| for (HgFile hgFile : changedFilesNotInCommit) { |
| filesNotIncludedString.append("<li>"); |
| filesNotIncludedString.append(hgFile.getRelativePath()); |
| filesNotIncludedString.append("</li>"); |
| } |
| if (!mayCommitEverything(filesNotIncludedString.toString())) { |
| //abort |
| return exceptions; |
| } |
| //firstly selected changes marked dirty in CommitHelper -> postRefresh, so we need to mark others |
| VcsDirtyScopeManager dirtyManager = VcsDirtyScopeManager.getInstance(myProject); |
| for (HgFile hgFile : changedFilesNotInCommit) { |
| dirtyManager.fileDirty(hgFile.toFilePath()); |
| } |
| } |
| // 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); |
| if (myShouldCommitSubrepos && repo.hasSubrepos()) { |
| command.setSubrepos(HgUtil.getNamesWithoutHashes(repo.getSubrepos())); |
| } |
| } |
| try { |
| command.execute(); |
| } catch (HgCommandException e) { |
| exceptions.add(new VcsException(e)); |
| } catch (VcsException e) { |
| exceptions.add(e); |
| } |
| } |
| |
| // push if needed |
| if (myNextCommitIsPushed && exceptions.isEmpty()) { |
| final List<HgRepository> preselectedRepositories = ContainerUtil.newArrayList(repositoriesMap.keySet()); |
| UIUtil.invokeLaterIfNeeded(new Runnable() { |
| public void run() { |
| new VcsPushDialog(myProject, preselectedRepositories).show(); |
| } |
| }); |
| } |
| |
| return exceptions; |
| } |
| |
| private boolean isMergeCommit(VirtualFile repo) { |
| return new HgWorkingCopyRevisionsCommand(myProject).parents(repo).size() > 1; |
| } |
| |
| private Set<HgFile> getChangedFilesNotInCommit(VirtualFile repo, Set<HgFile> selectedFiles) { |
| List<HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo); |
| |
| HgStatusCommand statusCommand = |
| new HgStatusCommand.Builder(true).unknown(false).ignored(false).baseRevision(parents.get(0)).build(myProject); |
| Set<HgChange> allChangedFilesInRepo = statusCommand.execute(repo); |
| |
| Set<HgFile> filesNotIncluded = new HashSet<HgFile>(); |
| |
| for (HgChange change : allChangedFilesInRepo) { |
| HgFile beforeFile = change.beforeFile(); |
| HgFile afterFile = change.afterFile(); |
| if (!selectedFiles.contains(beforeFile)) { |
| filesNotIncluded.add(beforeFile); |
| } else if (!selectedFiles.contains(afterFile)) { |
| filesNotIncluded.add(afterFile); |
| } |
| } |
| return filesNotIncluded; |
| } |
| |
| private boolean mayCommitEverything(final String filesNotIncludedString) { |
| final int[] choice = new int[1]; |
| Runnable runnable = new Runnable() { |
| public void run() { |
| choice[0] = Messages.showOkCancelDialog( |
| myProject, |
| HgVcsMessages.message("hg4idea.commit.partial.merge.message", filesNotIncludedString), |
| HgVcsMessages.message("hg4idea.commit.partial.merge.title"), |
| null |
| ); |
| } |
| }; |
| if (!ApplicationManager.getApplication().isDispatchThread()) { |
| ApplicationManager.getApplication().invokeAndWait(runnable, ModalityState.defaultModalityState()); |
| } else { |
| runnable.run(); |
| } |
| return choice[0] == Messages.OK; |
| } |
| |
| public List<VcsException> commit(List<Change> changes, String preparedComment) { |
| return commit(changes, preparedComment, FunctionUtil.nullConstant(), null); |
| } |
| |
| public List<VcsException> scheduleMissingFileForDeletion(List<FilePath> files) { |
| final List<HgFile> filesWithRoots = new ArrayList<HgFile>(); |
| for (FilePath filePath : files) { |
| VirtualFile vcsRoot = VcsUtil.getVcsRootFor(myProject, filePath); |
| if (vcsRoot == null) { |
| continue; |
| } |
| filesWithRoots.add(new HgFile(vcsRoot, filePath)); |
| } |
| new Task.Backgroundable(myProject, "Removing files...") { |
| @Override |
| public void run(@NotNull ProgressIndicator indicator) { |
| HgRemoveCommand command = new HgRemoveCommand(myProject); |
| command.execute(filesWithRoots); |
| } |
| }.queue(); |
| return null; |
| } |
| |
| public List<VcsException> scheduleUnversionedFilesForAddition(final List<VirtualFile> files) { |
| new HgAddCommand(myProject).addWithProgress(files); |
| return null; |
| } |
| |
| public boolean keepChangeListAfterCommit(ChangeList changeList) { |
| return false; |
| } |
| |
| @Override |
| public boolean isRefreshAfterCommitNeeded() { |
| return false; |
| } |
| |
| @NotNull |
| private Map<HgRepository, Set<HgFile>> getFilesByRepository(List<Change> changes) { |
| Map<HgRepository, Set<HgFile>> result = new HashMap<HgRepository, Set<HgFile>>(); |
| for (Change change : changes) { |
| ContentRevision afterRevision = change.getAfterRevision(); |
| ContentRevision beforeRevision = change.getBeforeRevision(); |
| |
| if (afterRevision != null) { |
| addFile(result, afterRevision); |
| } |
| if (beforeRevision != null) { |
| addFile(result, beforeRevision); |
| } |
| } |
| return result; |
| } |
| |
| private void addFile(Map<HgRepository, Set<HgFile>> result, ContentRevision contentRevision) { |
| FilePath filePath = contentRevision.getFile(); |
| // try to find repository from hgFile from change |
| HgRepository repo = HgUtil.getRepositoryForFile(myProject, contentRevision instanceof HgCurrentBinaryContentRevision |
| ? ((HgCurrentBinaryContentRevision)contentRevision).getRepositoryRoot() |
| : ChangesUtil.findValidParentAccurately(filePath)); |
| if (repo == null) { |
| return; |
| } |
| |
| Set<HgFile> hgFiles = result.get(repo); |
| if (hgFiles == null) { |
| hgFiles = new HashSet<HgFile>(); |
| result.put(repo, hgFiles); |
| } |
| |
| hgFiles.add(new HgFile(repo.getRoot(), filePath)); |
| } |
| |
| public void setNextCommitIsPushed() { |
| myNextCommitIsPushed = true; |
| } |
| |
| /** |
| * Commit options for hg |
| */ |
| private class HgCommitAdditionalComponent extends DvcsCommitAdditionalComponent { |
| @NotNull private final JCheckBox myCommitSubrepos; |
| |
| public HgCommitAdditionalComponent(@NotNull Project project, @NotNull CheckinProjectPanel panel) { |
| super(project, panel); |
| HgVcs myVcs = HgVcs.getInstance(myProject); |
| myAmend.setEnabled(myVcs != null && myVcs.getVersion().isAmendSupported()); |
| final Insets insets = new Insets(2, 2, 2, 2); |
| // add commit subrepos checkbox |
| GridBagConstraints c = new GridBagConstraints(); |
| c.anchor = GridBagConstraints.CENTER; |
| c.insets = insets; |
| c.gridx = 1; |
| c.gridy = 2; |
| c.weightx = 1; |
| c.fill = GridBagConstraints.HORIZONTAL; |
| myCommitSubrepos = new JCheckBox("Commit subrepositories", false); |
| myCommitSubrepos.setToolTipText(XmlStringUtil.wrapInHtml( |
| "Commit all subrepos for selected repositories.<br>" + |
| " <code>hg ci <i><b>files</b></i> -S <i><b>subrepos</b></i></code>")); |
| myCommitSubrepos.setMnemonic('s'); |
| myPanel.add(myCommitSubrepos, c); |
| Collection<HgRepository> repos = |
| HgActionUtil.collectRepositoriesFromFiles(HgUtil.getRepositoryManager(myProject), myCheckinPanel.getRoots()); |
| myCommitSubrepos.setVisible(ContainerUtil.exists(repos, new Condition<HgRepository>() { |
| @Override |
| public boolean value(HgRepository repository) { |
| return repository.hasSubrepos(); |
| } |
| })); |
| myCommitSubrepos.addActionListener(new MySelectionListener(myAmend)); |
| myAmend.addActionListener(new MySelectionListener(myCommitSubrepos)); |
| } |
| |
| @Override |
| public void refresh() { |
| super.refresh(); |
| restoreState(); |
| } |
| |
| @Override |
| public void saveState() { |
| myNextCommitAmend = myAmend.isSelected(); |
| myShouldCommitSubrepos = myCommitSubrepos.isSelected(); |
| } |
| |
| @Override |
| public void restoreState() { |
| myNextCommitAmend = false; |
| myShouldCommitSubrepos = false; |
| } |
| |
| @NotNull |
| @Override |
| protected Set<VirtualFile> getVcsRoots(@NotNull Collection<FilePath> filePaths) { |
| return HgUtil.hgRoots(myProject, filePaths); |
| } |
| |
| @Nullable |
| @Override |
| protected String getLastCommitMessage(@NotNull VirtualFile repo) throws VcsException { |
| HgCommandExecutor commandExecutor = new HgCommandExecutor(myProject); |
| List<String> args = new ArrayList<String>(); |
| args.add("-r"); |
| args.add("."); |
| args.add("--template"); |
| args.add("{desc}"); |
| HgCommandResult result = commandExecutor.executeInCurrentThread(repo, "log", args); |
| return result == null ? "" : result.getRawOutput(); |
| } |
| |
| private class MySelectionListener implements ActionListener { |
| JCheckBox myUnselectedComponent; |
| |
| public MySelectionListener(JCheckBox unselectedComponent) { |
| myUnselectedComponent = unselectedComponent; |
| } |
| |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| JCheckBox source = (JCheckBox)e.getSource(); |
| if (source.isSelected()) { |
| myUnselectedComponent.setSelected(false); |
| myUnselectedComponent.setEnabled(false); |
| } |
| else{ |
| myUnselectedComponent.setEnabled(true); |
| } |
| } |
| } |
| } |
| } |