/*
* 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.community.intellij.plugins.communitycase.checkout.branches;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager;
import com.intellij.openapi.vcs.changes.shelf.ShelvedChangeList;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.checkout.branches.BranchConfigurations.BranchChanges;
import org.community.intellij.plugins.communitycase.checkout.branches.BranchConfigurations.ChangeInfo;
import org.community.intellij.plugins.communitycase.checkout.branches.BranchConfigurations.ChangeListInfo;
import org.community.intellij.plugins.communitycase.commands.*;
import org.community.intellij.plugins.communitycase.history.HistoryUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.concurrent.Semaphore;
/**
* The checkout branch process. It used to organize the entire checkout process
*/
public class CheckoutProcess {
/**
* The logger
*/
private static final Logger LOG = Logger.getInstance("#"+CheckoutProcess.class.getName());
/**
* The configuration process
*/
final BranchConfigurations myConfig;
/**
* The vcs roots
*/
private List<VirtualFile> myRoots;
/**
* The vcs exceptions
*/
private List<VcsException> myExceptions;
/**
* The project
*/
private final Project myProject;
/**
* The shelve manager
*/
private final ShelveChangesManager myShelveManager;
/**
* The dirty scope manager
*/
private final VcsDirtyScopeManager myDirtyScopeManager;
/**
* The changes manager
*/
private final ChangeListManagerEx myChangeManager;
/**
* The project manager
*/
private final ProjectManagerEx myProjectManager;
/**
* The progress indicator
*/
private final ProgressIndicator myProgress;
/**
* The name of remote pseudo-configuration
*/
@Nullable private final String myRemoteConfiguration;
/**
* If true, the quick process is being used, and user is not offered to select changes
*/
private boolean myQuick;
/**
* The checkout process is run in the modify mode. Branch name or roots are changed.
*/
private final boolean myIsModify;
/**
* The new configuration
*/
private BranchConfiguration myNewConfiguration;
/**
* The new branch mapping
*/
private Map<VirtualFile, Pair<String, Boolean>> myNewBranchMapping = Collections.emptyMap();
/**
* The described roots
*/
private final Map<VirtualFile, String> myDescribedRoots = new HashMap<VirtualFile, String>();
/**
* If true, the checkout process was cancelled
*/
private boolean myCancelled;
/**
* The constructor
*
* @param config the configuration object
* @param project the context project
* @param shelveManager the shelve manager
* @param dirtyScopeManager the dirty scope manager
* @param changeManager the change manager
* @param projectManager the project manager
* @param progress the progress indicator for the process
* @param newConfiguration the new configuration, the current if modify
* @param quick if true, the no changes are assumed to be selected, so dialog need not be shown
*/
public CheckoutProcess(BranchConfigurations config,
Project project,
ShelveChangesManager shelveManager,
VcsDirtyScopeManager dirtyScopeManager,
ChangeListManagerEx changeManager,
ProjectManagerEx projectManager,
ProgressIndicator progress,
@Nullable BranchConfiguration newConfiguration,
@Nullable String remoteConfiguration,
boolean quick) {
myExceptions = Collections.synchronizedList(new ArrayList<VcsException>());
myConfig = config;
myProject = project;
myShelveManager = shelveManager;
myDirtyScopeManager = dirtyScopeManager;
myChangeManager = changeManager;
myProjectManager = projectManager;
myProgress = progress;
myNewConfiguration = newConfiguration;
myRemoteConfiguration = remoteConfiguration;
boolean f = false;
try {
f = myNewConfiguration == config.getCurrentConfiguration();
}
catch (VcsException e) {
myExceptions.add(e);
}
myIsModify = f;
myQuick = quick & !myIsModify;
}
/**
* Start the checkout process. The all activity is done in this thread. When needed, the activity is scheduled in awt or other threads.
*/
public void run() {
if (!myExceptions.isEmpty()) {
return;
}
myProgress.setText("Staring checkout...");
try {
myRoots = Util.getRoots(myConfig.getProject(), myConfig.getVcs());
saveAll();
waitForChanges();
myProjectManager.blockReloadingProjectOnExternalChanges();
try {
BranchConfiguration oldConfiguration = checkCurrentConfiguration();
if (oldConfiguration == null) {
myCancelled = true;
return;
}
for (VirtualFile root : myRoots) {
myDescribedRoots.put(root, myConfig.describeRoot(root));
}
ensureNoChangesInCurrent(oldConfiguration);
if (!myExceptions.isEmpty()) {
return;
}
List<Change> changes = collectChanges();
Collection<Change> selected;
myProgress.setText("Verifying the current configuration...");
if (myQuick && checkRoots()) {
// show switch dialog with new configuration that allows selecting changes (if not quick switch)
selected = Collections.emptyList();
}
else {
myProgress.setText("Selecting changes to transfer...");
selected = selectChangesToTransfer(changes);
if (selected == null) {
// the process was cancelled, or no changes that require checkout were made to the current configuration
myCancelled = true;
return;
}
}
// preparation phase finished. do actual checkout.
assert myNewConfiguration != null;
List<VirtualFile> checkoutRoots = rootsToCheckout();
if (myNewConfiguration != oldConfiguration || checkoutRoots.size() > 0) {
// TODO disable saving
myProgress.setText("Shelving changes...");
Pair<BranchChanges, BranchChanges> changesPair = shelveChanges(myProgress, oldConfiguration.getName(), selected);
try {
// save changes in old root, it also may be aliased with new root
oldConfiguration.setChanges(changesPair.first);
try {
if (checkoutRoots.size() > 0) {
HashSet<VirtualFile> startedRoots = new HashSet<VirtualFile>();
boolean failed = !checkoutAndRefreshRoots(checkoutRoots, startedRoots);
myProgress.setText2("");
if (!failed) {
myConfig.setCurrentConfiguration(myNewConfiguration);
}
else {
myNewConfiguration = oldConfiguration;
rollbackRootCheckout(startedRoots);
}
}
else {
myConfig.setCurrentConfiguration(myNewConfiguration);
}
}
finally {
final BranchChanges branchChanges = myNewConfiguration.getChanges();
myNewConfiguration.setChanges(null);
restoreChanges(myProgress, branchChanges);
}
}
finally {
// restore transient shelve
restoreChanges(myProgress, changesPair.second);
}
// TODO enable saving
}
}
finally {
myProjectManager.unblockReloadingProjectOnExternalChanges();
}
// launch project update?
}
catch (VcsException e) {
myExceptions.add(e);
}
catch (Throwable e) {
myExceptions.add(new VcsException("The checkout process failed: " + e.getMessage(), e));
}
finally {
saveAll(); // saves configuration changes
}
}
/**
* Ensure that current configuration contains no changes. This is a bug condition.
*
* @param oldConfiguration the old configuration
*/
private void ensureNoChangesInCurrent(BranchConfiguration oldConfiguration) {
final BranchChanges oldChanges = oldConfiguration.getChanges();
if (oldChanges != null) {
String name = oldChanges.SHELVE_PATH;
for (ShelvedChangeList changeList : myShelveManager.getShelvedChangeLists()) {
if (changeList.PATH.equals(oldChanges.SHELVE_PATH)) {
name = changeList.DESCRIPTION;
break;
}
}
final VcsException ex = new VcsException("The current configuration contains shelve: " + name);
LOG.error(ex);
myExceptions.add(ex);
oldConfiguration.setChanges(null);
}
}
/**
* Rollback the root checkout
*
* @param startedRoots that roots that has been started and need to be rolled back
*/
private void rollbackRootCheckout(HashSet<VirtualFile> startedRoots) {
myProgress.setText("Rolling back...");
for (VirtualFile root : startedRoots) {
myProgress.setText2(root.getPath());
startedRoots.add(root);
LineHandler h = new LineHandler(myProject, root, Command.GIT_CHECKOUT);
h.setRemote(true);
h.addParameters("-f", myDescribedRoots.get(root));
Collection<VcsException> exceptions = HandlerUtil.doSynchronouslyWithExceptions(h, myProgress, h.printableCommandLine());
myExceptions.addAll(exceptions);
}
// filter out implicitly included roots
ArrayList<VirtualFile> newRoots = new ArrayList<VirtualFile>();
loop:
for (VirtualFile root : startedRoots) {
for (VirtualFile other : startedRoots) {
if (other != root && VfsUtil.isAncestor(other, root, true)) {
continue loop;
}
}
newRoots.add(root);
}
for (VirtualFile root : newRoots) {
root.refresh(false, true);
}
myProgress.setText2("");
}
/**
* Checkout needed vcs roots and do vcs refresh on diff files
*
* @param checkoutRoots the roots to checkout
* @param startedRoots the roots for which checkout actually started (Modified by this method)
* @return true if checkout is successful
* @throws VcsException
*/
private boolean checkoutAndRefreshRoots(List<VirtualFile> checkoutRoots, HashSet<VirtualFile> startedRoots) throws VcsException {
boolean failed = false;
myProgress.setText("Checking out...");
HashSet<File> filesToRefresh = new HashSet<File>();
for (VirtualFile root : checkoutRoots) {
myProgress.setText2(root.getPath());
startedRoots.add(root);
VcsRevisionNumber prev=HistoryUtils.getCurrentRevision(myProject,root);
LineHandler h = new LineHandler(myProject, root, Command.GIT_CHECKOUT);
h.addParameters("-f");
Pair<String, Boolean> branchedRef = myNewBranchMapping.get(root);
String ref = myNewConfiguration.getReference(root.getPath());
h.addParameters("-l");
if (branchedRef != null) {
if (branchedRef.second) {
h.addParameters("-t");
}
h.addParameters("-b", ref, branchedRef.first);
}
else {
h.addParameters(ref);
}
Collection<VcsException> exceptions = HandlerUtil.doSynchronouslyWithExceptions(h, myProgress, h.printableCommandLine());
if (!exceptions.isEmpty()) {
myExceptions.addAll(exceptions);
failed = true;
break;
}
SimpleHandler d = new SimpleHandler(myProject, root, Command.DIFF);
d.addParameters("--name-only", prev.asString() + "..HEAD");
d.setRemote(true);
d.setSilent(true);
d.endOptions();
try {
File base = new File(root.getPath());
for (StringScanner s = new StringScanner(d.run()); s.hasMoreData();) {
String l = s.line();
if (l.length() > 0) {
filesToRefresh.add(new File(base, Util.unescapePath(l)));
}
}
}
catch (VcsException e) {
LOG.error("Unexpected diff failure", e);
myExceptions.add(e);
failed = true;
break;
}
}
if (!failed) {
LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh);
}
return !failed;
}
/**
* @return set of roots to checkout
*/
private List<VirtualFile> rootsToCheckout() {
ArrayList<VirtualFile> rc = new ArrayList<VirtualFile>();
for (VirtualFile root : myRoots) {
String current = myDescribedRoots.get(root);
String newRef = myNewConfiguration.getReference(root.getPath());
if (!current.equals(newRef)) {
rc.add(root);
}
}
return rc;
}
/**
* @param changes all changes
* @return this method allows renaming and updating, the branch configuration and to select changes to transfer to new configuration.
* null if process is cancelled, or only name changed in the current configuration and no checkout is required.
*/
@Nullable
private Collection<Change> selectChangesToTransfer(final List<Change> changes) {
final Ref<SwitchBranchesDialog.Result> t = new Ref<SwitchBranchesDialog.Result>();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
try {
t.set(SwitchBranchesDialog
.showDialog(myProject, myNewConfiguration, changes, myRoots, myRemoteConfiguration, myConfig, myIsModify));
}
catch (VcsException e) {
myExceptions.add(e);
}
catch (Throwable e) {
LOG.error("Unexpected error", e);
myExceptions.add(new VcsException("Selecting changes failed", e));
}
}
});
if (t.get() == null) {
return null;
}
else {
myNewBranchMapping = t.get().referencesToUse;
myNewConfiguration = t.get().target;
return t.get().changes;
}
}
/**
* @return true if roots in the new configuration are configured correctly
*/
private boolean checkRoots() { //todo wc check if this still works after removing RevisionNumber.resolve
if (myNewConfiguration == null) {
return false;
}
LocalFileSystem lfs = LocalFileSystem.getInstance();
HashSet<VirtualFile> roots = new HashSet<VirtualFile>(myRoots);
Map<String, String> branches = myNewConfiguration.getReferences();
for (Map.Entry<String, String> m : branches.entrySet()) {
VirtualFile root = lfs.findFileByPath(m.getKey());
if (root == null || root.findChild(".") == null || !roots.contains(root)) {
return false;
}
roots.remove(root);
}
return roots.isEmpty();
}
/**
* @return collect changes from project
*/
private List<Change> collectChanges() {
List<LocalChangeList> changeLists = myChangeManager.getChangeLists();
ArrayList<Change> changes = new ArrayList<Change>();
for (LocalChangeList l : changeLists) {
changes.addAll(l.getChanges());
}
return changes;
}
/**
* @return the current configuration, or null if process was cancelled
* @throws VcsException if there is a problem with checking configuration
*/
@Nullable
private BranchConfiguration checkCurrentConfiguration() throws VcsException {
BranchConfiguration c = myConfig.getCurrentConfiguration();
if (!myIsModify && !rootsMatchConfiguration(c)) {
// when the current configuration is modified, there is no need to show duplicate dialogs
c = BranchConfigurationChangedDialog.showDialog(myConfig, c, myRoots);
}
return c;
}
/**
* Check of the root mapping has been changed for the current vcs root
*
* @param c the configuration to check
* @return true if nothing shoudlbe checked out
* @throws VcsException
*/
private boolean rootsMatchConfiguration(BranchConfiguration c) throws VcsException {
LocalFileSystem lfs = LocalFileSystem.getInstance();
Map<String, String> branches = c.getReferences();
if (branches.size() != myRoots.size()) {
return false;
}
HashSet<String> realSet = new HashSet<String>();
for (VirtualFile root : myRoots) {
realSet.add(root.getPath());
}
HashSet<String> storedSet = new HashSet<String>();
for (Map.Entry<String, String> m : branches.entrySet()) {
String rootPath = m.getKey();
storedSet.add(rootPath);
VirtualFile root = lfs.findFileByPath(rootPath);
String ref = myConfig.describeRoot(root);
if (!ref.equals(m.getValue())) {
return false;
}
}
return storedSet.equals(realSet);
}
private void waitForChanges() throws VcsException {
final Semaphore s = new Semaphore(0);
waitForChangesRefresh("Preparing for the checkout: ", new Runnable() {
@Override
public void run() {
s.release();
}
});
try {
s.acquire();
}
catch (InterruptedException e) {
throw new VcsException("Waiting for changes was interrupted: ", e);
}
}
/**
* Save all changes before start of update process
*/
private static void saveAll() {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
FileDocumentManager.getInstance().saveAllDocuments();
}
});
}
});
}
/**
* Restore changes.
*
* @param progress the progress indicator
* @param changes the changes to restore, null means no changes to restore
* @return true if changes has been restored successfully
*/
private boolean restoreChanges(ProgressIndicator progress,
final BranchChanges changes) {
if (changes == null) {
return true;
}
ShelvedChangeList shelve = null;
for (ShelvedChangeList changeList : myShelveManager.getShelvedChangeLists()) {
if (changeList.PATH.equals(changes.SHELVE_PATH)) {
shelve = changeList;
}
}
if (shelve == null) {
//noinspection ThrowableInstanceNeverThrown
myExceptions.add(new VcsException("Failed to find shelve with path" + changes.SHELVE_PATH));
return false;
}
progress.setText("Refreshing files before restoring shelve: " + shelve.DESCRIPTION);
//StashUtils.doSystemUnshelve(myProject, shelve, myShelveManager, myChangeManager, myExceptions);
//todo wc do unshelf here
// dirty files and parse changes
final HashMap<Pair<String, String>, String> parsedChanges = new HashMap<Pair<String, String>, String>();
for (ChangeInfo changeInfo : changes.CHANGES) {
String before = changeInfo.BEFORE_PATH;
String after = changeInfo.AFTER_PATH;
parsedChanges.put(Pair.create(before, after), changeInfo.CHANGE_LIST_NAME);
if (after != null) {
myDirtyScopeManager.fileDirty(VcsUtil.getFilePath(after));
}
if (before != null) {
myDirtyScopeManager.fileDirty(VcsUtil.getFilePath(before));
}
}
final ShelvedChangeList finalShelve = shelve;
try {
waitForChanges();
HashMap<String, LocalChangeList> lists = new HashMap<String, LocalChangeList>();
for (LocalChangeList localChangeList : myChangeManager.getChangeLists()) {
lists.put(localChangeList.getName(), localChangeList);
}
LocalChangeList defaultList = myChangeManager.getDefaultChangeList();
for (ChangeListInfo changeListInfo : changes.CHANGE_LISTS) {
LocalChangeList changeList = lists.get(changeListInfo.NAME);
if (changeList == null) {
changeList = myChangeManager.addChangeList(changeListInfo.NAME, changeListInfo.COMMENT);
lists.put(changeListInfo.NAME, changeList);
}
if (changeListInfo.IS_DEFAULT) {
myChangeManager.setDefaultChangeList(changeList);
}
}
for (Change change : defaultList.getChanges()) {
ContentRevision beforeRevision = change.getBeforeRevision();
String before = beforeRevision == null ? null : beforeRevision.getFile().getPath();
ContentRevision afterRevision = change.getAfterRevision();
String after = afterRevision == null ? null : afterRevision.getFile().getPath();
Pair<String, String> key = Pair.create(before, after);
String listName = parsedChanges.get(key);
assert listName != null : "List name should be found: " + key;
if (!listName.equals(defaultList.getName())) {
LocalChangeList changeList = lists.get(listName);
assert changeList != null : "Change List should be found: " + listName;
myChangeManager.moveChangesTo(changeList, new Change[]{change});
}
}
return true;
}
catch (Throwable t) {
//noinspection ThrowableInstanceNeverThrown
myExceptions.add(
new VcsException("Failed to process restore shelved change list: " + finalShelve.DESCRIPTION + ". Please restore it manually.", t));
return false;
}
}
/**
* Wait until changes are refreshed
*
* @param title the title of the operation
* @param runnable the process that awaits changes
*/
void waitForChangesRefresh(final String title, final Runnable runnable) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
myChangeManager.invokeAfterUpdate(runnable, InvokeAfterUpdateMode.BACKGROUND_NOT_CANCELLABLE, title,
ModalityState.NON_MODAL);
}
});
}
/**
* Shelve selected and transient changes. If creation of one of shelves fails, the other shelve is rolled back
*
* @param configurationName the current configuration name
* @param selectedChanges the selected changes
* @return null if operation fails or pair if operation completed successfully. The first is changes to be stored in old configuration, the second is trainsient changes.
*/
@Nullable
private Pair<BranchChanges, BranchChanges> shelveChanges(ProgressIndicator progress,
String configurationName,
Collection<Change> selectedChanges) {
assert myExceptions.isEmpty() : "The method should not be called if there is already problems detected";
List<LocalChangeList> changeLists = myChangeManager.getChangeListsCopy();
HashMap<Change, LocalChangeList> changes = new HashMap<Change, LocalChangeList>();
for (LocalChangeList l : changeLists) {
for (Change change : l.getChanges()) {
changes.put(change, l);
}
}
HashSet<Change> selected = new HashSet<Change>(selectedChanges);
HashSet<Change> other = new HashSet<Change>(changes.keySet());
other.removeAll(selected);
Date now = new Date();
BranchChanges storedChanges = shelveChanges(progress, changes, other,
"Shelved changes for configuration " + configurationName + " (created on " + now + ")");
if (!myExceptions.isEmpty()) {
return null;
}
BranchChanges transientChanges = shelveChanges(progress, changes, selected,
"Transferred changes from configuration " +
configurationName +
" (created on " +
now +
")");
if (!myExceptions.isEmpty()) {
restoreChanges(progress, storedChanges);
return null;
}
return Pair.create(storedChanges, transientChanges);
}
/**
* Shelve changes remembering change configuration
*
* @param progress the progress
* @param changes the all changes to process
* @param toShelve the shelved change subset
* @param description the description of the shelve
* @return branch change set or null if shelve is not created (there will be exceptions in list in case of errors)
*/
@Nullable
private BranchChanges shelveChanges(ProgressIndicator progress,
HashMap<Change, LocalChangeList> changes,
HashSet<Change> toShelve, String description
) {
if (toShelve.isEmpty()) {
return null;
}
HashSet<LocalChangeList> lists = new HashSet<LocalChangeList>();
ArrayList<ChangeInfo> ci = new ArrayList<ChangeInfo>(toShelve.size());
for (Change c : toShelve) {
LocalChangeList l = changes.get(c);
lists.add(l);
ChangeInfo i = new ChangeInfo();
ContentRevision after = c.getAfterRevision();
if (after != null) {
i.AFTER_PATH = after.getFile().getPath();
}
ContentRevision before = c.getBeforeRevision();
if (before != null) {
i.BEFORE_PATH = before.getFile().getPath();
}
i.CHANGE_LIST_NAME = l.getName();
ci.add(i);
}
ArrayList<ChangeListInfo> li = new ArrayList<ChangeListInfo>(lists.size());
for (LocalChangeList l : lists) {
ChangeListInfo i = new ChangeListInfo();
i.IS_DEFAULT = l.isDefault();
i.NAME = l.getName();
i.COMMENT = l.getComment();
li.add(i);
}
if (progress != null) {
progress.setText("Creating shelve: " + description);
}
//ShelvedChangeList shelved = StashUtils.shelveChanges(myProject, myShelveManager, toShelve, description, myExceptions);
//todo wc do shelf here
return null;
/*
if (shelved == null) {
return null;
}
BranchChanges b = new BranchChanges();
b.SHELVE_PATH = shelved.PATH;
b.CHANGE_LISTS = li.toArray(new ChangeListInfo[li.size()]);
b.CHANGES = ci.toArray(new ChangeInfo[ci.size()]);
return b;
*/
}
/**
* @return the list of problems
*/
public List<VcsException> getExceptions() {
return myExceptions;
}
/**
* @return true if the process was cancelled
*/
public boolean isCancelled() {
return myCancelled;
}
/**
* @return true if the process was modification process
*/
public boolean isModify() {
return myIsModify;
}
}