| /* |
| * Copyright 2000-2013 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 git4idea.commands; |
| |
| import com.intellij.execution.ExecutionException; |
| import com.intellij.execution.configurations.GeneralCommandLine; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.vcs.FilePath; |
| import com.intellij.openapi.vcs.ProcessEventListener; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vfs.LocalFileSystem; |
| import com.intellij.openapi.vfs.VfsUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.util.EventDispatcher; |
| import com.intellij.util.Processor; |
| import com.intellij.vcsUtil.VcsFileUtil; |
| import git4idea.GitVcs; |
| import git4idea.config.GitVcsApplicationSettings; |
| import git4idea.config.GitVcsSettings; |
| import git4idea.config.GitVersionSpecialty; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.git4idea.ssh.GitSSHHandler; |
| import org.jetbrains.git4idea.ssh.GitSSHService; |
| |
| import java.io.File; |
| import java.io.OutputStream; |
| import java.nio.charset.Charset; |
| import java.util.*; |
| import java.util.concurrent.LinkedBlockingQueue; |
| |
| /** |
| * A handler for git commands |
| */ |
| public abstract class GitHandler { |
| protected final Project myProject; |
| protected final GitCommand myCommand; |
| |
| private final HashSet<Integer> myIgnoredErrorCodes = new HashSet<Integer>(); // Error codes that are ignored for the handler |
| private final List<VcsException> myErrors = Collections.synchronizedList(new ArrayList<VcsException>()); |
| private final List<String> myLastOutput = Collections.synchronizedList(new ArrayList<String>()); |
| private final int LAST_OUTPUT_SIZE = 5; |
| protected static final Logger LOG = Logger.getInstance(GitHandler.class.getName()); |
| final GeneralCommandLine myCommandLine; |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| Process myProcess; |
| |
| private boolean myStdoutSuppressed; // If true, the standard output is not copied to version control console |
| private boolean myStderrSuppressed; // If true, the standard error is not copied to version control console |
| private final File myWorkingDirectory; |
| |
| private boolean myEnvironmentCleanedUp = true; // the flag indicating that environment has been cleaned up, by default is true because there is nothing to clean |
| private int myHandlerNo; |
| private Processor<OutputStream> myInputProcessor; // The processor for stdin |
| |
| // if true process might be cancelled |
| // note that access is safe because it accessed in unsynchronized block only after process is started, and it does not change after that |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| private boolean myIsCancellable = true; |
| |
| private Integer myExitCode; // exit code or null if exit code is not yet available |
| |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| @NonNls |
| private Charset myCharset = Charset.forName("UTF-8"); // Character set to use for IO |
| |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| private boolean myNoSSHFlag = false; |
| |
| private final EventDispatcher<ProcessEventListener> myListeners = EventDispatcher.create(ProcessEventListener.class); |
| @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) |
| protected boolean mySilent; // if true, the command execution is not logged in version control view |
| |
| protected final GitVcs myVcs; |
| private final Map<String, String> myEnv; |
| private GitVcsApplicationSettings myAppSettings; |
| private GitVcsSettings myProjectSettings; |
| |
| private Runnable mySuspendAction; // Suspend action used by {@link #suspendWriteLock()} |
| private Runnable myResumeAction; // Resume action used by {@link #resumeWriteLock()} |
| |
| private long myStartTime; // git execution start timestamp |
| private static final long LONG_TIME = 10 * 1000; |
| @Nullable private ModalityState myState; |
| |
| |
| /** |
| * A constructor |
| * |
| * @param project a project |
| * @param directory a process directory |
| * @param command a command to execute (if empty string, the parameter is ignored) |
| */ |
| protected GitHandler(@NotNull Project project, @NotNull File directory, @NotNull GitCommand command) { |
| myProject = project; |
| myCommand = command; |
| myAppSettings = GitVcsApplicationSettings.getInstance(); |
| myProjectSettings = GitVcsSettings.getInstance(myProject); |
| myEnv = new HashMap<String, String>(System.getenv()); |
| myVcs = GitVcs.getInstance(project); |
| myWorkingDirectory = directory; |
| myCommandLine = new GeneralCommandLine(); |
| if (myAppSettings != null) { |
| myCommandLine.setExePath(myAppSettings.getPathToGit()); |
| } |
| myCommandLine.setWorkDirectory(myWorkingDirectory); |
| if (command.name().length() > 0) { |
| myCommandLine.addParameter(command.name()); |
| } |
| } |
| |
| /** |
| * A constructor |
| * |
| * @param project a project |
| * @param vcsRoot a process directory |
| * @param command a command to execute |
| */ |
| protected GitHandler(final Project project, final VirtualFile vcsRoot, final GitCommand command) { |
| this(project, VfsUtil.virtualToIoFile(vcsRoot), command); |
| } |
| |
| /** |
| * @return multicaster for listeners |
| */ |
| protected ProcessEventListener listeners() { |
| return myListeners.getMulticaster(); |
| } |
| |
| /** |
| * Add error code to ignored list |
| * |
| * @param code the code to ignore |
| */ |
| public void ignoreErrorCode(int code) { |
| myIgnoredErrorCodes.add(code); |
| } |
| |
| /** |
| * Check if error code should be ignored |
| * |
| * @param code a code to check |
| * @return true if error code is ignorable |
| */ |
| public boolean isIgnoredErrorCode(int code) { |
| return myIgnoredErrorCodes.contains(code); |
| } |
| |
| |
| /** |
| * add error to the error list |
| * |
| * @param ex an error to add to the list |
| */ |
| public void addError(VcsException ex) { |
| myErrors.add(ex); |
| } |
| |
| public void addLastOutput(String line) { |
| if (myLastOutput.size() < LAST_OUTPUT_SIZE) { |
| myLastOutput.add(line); |
| } else { |
| myLastOutput.add(0, line); |
| Collections.rotate(myLastOutput, -1); |
| } |
| } |
| |
| public List<String> getLastOutput() { |
| return myLastOutput; |
| } |
| |
| /** |
| * @return unmodifiable list of errors. |
| */ |
| public List<VcsException> errors() { |
| return Collections.unmodifiableList(myErrors); |
| } |
| |
| /** |
| * @return a context project |
| */ |
| public Project project() { |
| return myProject; |
| } |
| |
| /** |
| * @return the current working directory |
| */ |
| public File workingDirectory() { |
| return myWorkingDirectory; |
| } |
| |
| /** |
| * @return the current working directory |
| */ |
| public VirtualFile workingDirectoryFile() { |
| final VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(workingDirectory()); |
| if (file == null) { |
| throw new IllegalStateException("The working directly should be available: " + workingDirectory()); |
| } |
| return file; |
| } |
| |
| /** |
| * Set SSH flag. This flag should be set to true for commands that never interact with remote repositories. |
| * |
| * @param value if value is true, the custom ssh is not used for the command. |
| */ |
| @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) |
| public void setNoSSH(boolean value) { |
| checkNotStarted(); |
| myNoSSHFlag = value; |
| } |
| |
| /** |
| * @return true if SSH is not invoked by this command. |
| */ |
| @SuppressWarnings({"WeakerAccess"}) |
| public boolean isNoSSH() { |
| return myNoSSHFlag; |
| } |
| |
| /** |
| * Add listener to handler |
| * |
| * @param listener a listener |
| */ |
| protected void addListener(ProcessEventListener listener) { |
| myListeners.addListener(listener); |
| } |
| |
| /** |
| * End option parameters and start file paths. The method adds {@code "--"} parameter. |
| */ |
| public void endOptions() { |
| myCommandLine.addParameter("--"); |
| } |
| |
| /** |
| * Add string parameters |
| * |
| * @param parameters a parameters to add |
| */ |
| @SuppressWarnings({"WeakerAccess"}) |
| public void addParameters(@NonNls @NotNull String... parameters) { |
| addParameters(Arrays.asList(parameters)); |
| } |
| |
| /** |
| * Add parameters from the list |
| * |
| * @param parameters the parameters to add |
| */ |
| public void addParameters(List<String> parameters) { |
| checkNotStarted(); |
| for (String parameter : parameters) { |
| myCommandLine.addParameter(escapeParameterIfNeeded(parameter)); |
| } |
| } |
| |
| @NotNull |
| private String escapeParameterIfNeeded(@NotNull String parameter) { |
| if (escapeNeeded(parameter)) { |
| return parameter.replaceAll("\\^", "^^^^"); |
| } |
| return parameter; |
| } |
| |
| private boolean escapeNeeded(@NotNull String parameter) { |
| return SystemInfo.isWindows && isCmd() && parameter.contains("^"); |
| } |
| |
| private boolean isCmd() { |
| return myAppSettings.getPathToGit().toLowerCase().endsWith("cmd"); |
| } |
| |
| @NotNull |
| private String unescapeCommandLine(@NotNull String commandLine) { |
| if (escapeNeeded(commandLine)) { |
| return commandLine.replaceAll("\\^\\^\\^\\^", "^"); |
| } |
| return commandLine; |
| } |
| |
| /** |
| * Add file path parameters. The parameters are made relative to the working directory |
| * |
| * @param parameters a parameters to add |
| * @throws IllegalArgumentException if some path is not under root. |
| */ |
| public void addRelativePaths(@NotNull FilePath... parameters) { |
| addRelativePaths(Arrays.asList(parameters)); |
| } |
| |
| /** |
| * Add file path parameters. The parameters are made relative to the working directory |
| * |
| * @param filePaths a parameters to add |
| * @throws IllegalArgumentException if some path is not under root. |
| */ |
| @SuppressWarnings({"WeakerAccess"}) |
| public void addRelativePaths(@NotNull final Collection<FilePath> filePaths) { |
| checkNotStarted(); |
| for (FilePath path : filePaths) { |
| myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, path)); |
| } |
| } |
| |
| /** |
| * Add file path parameters. The parameters are made relative to the working directory |
| * |
| * @param files a parameters to add |
| * @throws IllegalArgumentException if some path is not under root. |
| */ |
| public void addRelativePathsForFiles(@NotNull final Collection<File> files) { |
| checkNotStarted(); |
| for (File file : files) { |
| myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, file)); |
| } |
| } |
| |
| /** |
| * Add virtual file parameters. The parameters are made relative to the working directory |
| * |
| * @param files a parameters to add |
| * @throws IllegalArgumentException if some path is not under root. |
| */ |
| @SuppressWarnings({"WeakerAccess"}) |
| public void addRelativeFiles(@NotNull final Collection<VirtualFile> files) { |
| checkNotStarted(); |
| for (VirtualFile file : files) { |
| myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, file)); |
| } |
| } |
| |
| /** |
| * Adds "--progress" parameter. Usable for long operations, such as clone or fetch. |
| * @return is "--progress" parameter supported by this version of Git. |
| */ |
| public boolean addProgressParameter() { |
| if (GitVersionSpecialty.ABLE_TO_USE_PROGRESS_IN_REMOTE_COMMANDS.existsIn(myVcs.getVersion())) { |
| addParameters("--progress"); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * check that process is not started yet |
| * |
| * @throws IllegalStateException if process has been already started |
| */ |
| private void checkNotStarted() { |
| if (isStarted()) { |
| throw new IllegalStateException("The process has been already started"); |
| } |
| } |
| |
| /** |
| * check that process is started |
| * |
| * @throws IllegalStateException if process has not been started |
| */ |
| protected final void checkStarted() { |
| if (!isStarted()) { |
| throw new IllegalStateException("The process is not started yet"); |
| } |
| } |
| |
| /** |
| * @return true if process is started |
| */ |
| public final synchronized boolean isStarted() { |
| return myProcess != null; |
| } |
| |
| |
| /** |
| * Set new value of cancellable flag (by default true) |
| * |
| * @param value a new value of the flag |
| */ |
| public void setCancellable(boolean value) { |
| checkNotStarted(); |
| myIsCancellable = value; |
| } |
| |
| /** |
| * @return cancellable state |
| */ |
| public boolean isCancellable() { |
| return myIsCancellable; |
| } |
| |
| /** |
| * Start process |
| */ |
| public synchronized void start() { |
| checkNotStarted(); |
| |
| try { |
| myStartTime = System.currentTimeMillis(); |
| if (!myProject.isDefault() && !mySilent && (myVcs != null)) { |
| myVcs.showCommandLine("cd " + myWorkingDirectory); |
| myVcs.showCommandLine(printableCommandLine()); |
| LOG.info("cd " + myWorkingDirectory); |
| LOG.info(printableCommandLine()); |
| } |
| else { |
| LOG.debug("cd " + myWorkingDirectory); |
| LOG.debug(printableCommandLine()); |
| } |
| |
| // setup environment |
| if (!myNoSSHFlag && myProjectSettings.isIdeaSsh()) { |
| GitSSHService ssh = GitSSHIdeaService.getInstance(); |
| myEnv.put(GitSSHHandler.GIT_SSH_ENV, ssh.getScriptPath().getPath()); |
| myHandlerNo = ssh.registerHandler(new GitSSHGUIHandler(myProject, myState)); |
| myEnvironmentCleanedUp = false; |
| myEnv.put(GitSSHHandler.SSH_HANDLER_ENV, Integer.toString(myHandlerNo)); |
| int port = ssh.getXmlRcpPort(); |
| myEnv.put(GitSSHHandler.SSH_PORT_ENV, Integer.toString(port)); |
| LOG.debug(String.format("handler=%s, port=%s", myHandlerNo, port)); |
| } |
| myCommandLine.setEnvParams(myEnv); |
| // start process |
| myProcess = startProcess(); |
| startHandlingStreams(); |
| } |
| catch (Throwable t) { |
| cleanupEnv(); |
| myListeners.getMulticaster().startFailed(t); |
| } |
| } |
| |
| protected abstract Process startProcess() throws ExecutionException; |
| |
| /** |
| * Start handling process output streams for the handler. |
| */ |
| protected abstract void startHandlingStreams(); |
| |
| /** |
| * @return a command line with full path to executable replace to "git" |
| */ |
| public String printableCommandLine() { |
| return unescapeCommandLine(myCommandLine.getCommandLineString("git")); |
| } |
| |
| /** |
| * Cancel activity |
| */ |
| public synchronized void cancel() { |
| checkStarted(); |
| if (!myIsCancellable) { |
| throw new IllegalStateException("The process is not cancellable."); |
| } |
| destroyProcess(); |
| } |
| |
| /** |
| * Destroy process |
| */ |
| public abstract void destroyProcess(); |
| |
| /** |
| * @return exit code for process if it is available |
| */ |
| public synchronized int getExitCode() { |
| if (myExitCode == null) { |
| throw new IllegalStateException("Exit code is not yet available"); |
| } |
| return myExitCode.intValue(); |
| } |
| |
| /** |
| * @param exitCode a exit code for process |
| */ |
| protected synchronized void setExitCode(int exitCode) { |
| myExitCode = exitCode; |
| } |
| |
| /** |
| * Cleanup environment |
| */ |
| protected synchronized void cleanupEnv() { |
| if (!myNoSSHFlag && !myEnvironmentCleanedUp) { |
| GitSSHService ssh = GitSSHIdeaService.getInstance(); |
| myEnvironmentCleanedUp = true; |
| ssh.unregisterHandler(myHandlerNo); |
| } |
| } |
| |
| /** |
| * Wait for process termination |
| */ |
| public void waitFor() { |
| checkStarted(); |
| try { |
| if (myInputProcessor != null && myProcess != null) { |
| myInputProcessor.process(myProcess.getOutputStream()); |
| } |
| } |
| finally { |
| waitForProcess(); |
| } |
| } |
| |
| /** |
| * Wait for process |
| */ |
| protected abstract void waitForProcess(); |
| |
| /** |
| * Set silent mode. When handler is silent, it does not logs command in version control console. |
| * Note that this option also suppresses stderr and stdout copying. |
| * |
| * @param silent a new value of the flag |
| * @see #setStderrSuppressed(boolean) |
| * @see #setStdoutSuppressed(boolean) |
| */ |
| @SuppressWarnings({"SameParameterValue"}) |
| public void setSilent(final boolean silent) { |
| checkNotStarted(); |
| mySilent = silent; |
| setStderrSuppressed(silent); |
| setStdoutSuppressed(silent); |
| } |
| |
| /** |
| * @return a character set to use for IO |
| */ |
| public Charset getCharset() { |
| return myCharset; |
| } |
| |
| /** |
| * Set character set for IO |
| * |
| * @param charset a character set |
| */ |
| @SuppressWarnings({"SameParameterValue"}) |
| public void setCharset(final Charset charset) { |
| myCharset = charset; |
| } |
| |
| /** |
| * @return true if standard output is not copied to the console |
| */ |
| public boolean isStdoutSuppressed() { |
| return myStdoutSuppressed; |
| } |
| |
| /** |
| * Set flag specifying if stdout should be copied to the console |
| * |
| * @param stdoutSuppressed true if output is not copied to the console |
| */ |
| public void setStdoutSuppressed(final boolean stdoutSuppressed) { |
| checkNotStarted(); |
| myStdoutSuppressed = stdoutSuppressed; |
| } |
| |
| /** |
| * @return true if standard output is not copied to the console |
| */ |
| public boolean isStderrSuppressed() { |
| return myStderrSuppressed; |
| } |
| |
| /** |
| * Set flag specifying if stderr should be copied to the console |
| * |
| * @param stderrSuppressed true if error output is not copied to the console |
| */ |
| public void setStderrSuppressed(final boolean stderrSuppressed) { |
| checkNotStarted(); |
| myStderrSuppressed = stderrSuppressed; |
| } |
| |
| /** |
| * Set environment variable |
| * |
| * @param name the variable name |
| * @param value the variable value |
| */ |
| public void setEnvironment(String name, String value) { |
| myEnv.put(name, value); |
| } |
| |
| /** |
| * Set processor for standard input. This is a place where input to the git application could be generated. |
| * |
| * @param inputProcessor the processor |
| */ |
| public void setInputProcessor(Processor<OutputStream> inputProcessor) { |
| myInputProcessor = inputProcessor; |
| } |
| |
| /** |
| * Set suspend/resume actions |
| * |
| * @param suspend the suspend action |
| * @param resume the resume action |
| */ |
| synchronized void setSuspendResume(Runnable suspend, Runnable resume) { |
| mySuspendAction = suspend; |
| myResumeAction = resume; |
| } |
| |
| /** |
| * Suspend write lock held by the handler |
| */ |
| public synchronized void suspendWriteLock() { |
| assert mySuspendAction != null; |
| mySuspendAction.run(); |
| } |
| |
| /** |
| * Resume write lock held by the handler |
| */ |
| public synchronized void resumeWriteLock() { |
| assert mySuspendAction != null; |
| myResumeAction.run(); |
| } |
| |
| public void setModalityState(@Nullable ModalityState state) { |
| myState = state; |
| } |
| |
| /** |
| * @return true if the command line is too big |
| */ |
| public boolean isLargeCommandLine() { |
| return myCommandLine.getCommandLineString().length() > VcsFileUtil.FILE_PATH_LIMIT; |
| } |
| |
| public void runInCurrentThread(@Nullable Runnable postStartAction) { |
| //LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread(), "Git process should never start in the dispatch thread."); |
| |
| final GitVcs vcs = GitVcs.getInstance(myProject); |
| if (vcs == null) { return; } |
| |
| boolean suspendable = false; |
| switch (myCommand.lockingPolicy()) { |
| case READ: |
| // need to lock only write operations: reads can be performed even when a write operation is going on |
| break; |
| case WRITE_SUSPENDABLE: |
| suspendable = true; |
| //noinspection fallthrough |
| case WRITE: |
| vcs.getCommandLock().writeLock().lock(); |
| break; |
| } |
| try { |
| if (suspendable) { |
| final Object EXIT = new Object(); |
| final Object SUSPEND = new Object(); |
| final Object RESUME = new Object(); |
| final LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<Object>(); |
| Runnable suspend = new Runnable() { |
| public void run() { |
| queue.add(SUSPEND); |
| } |
| }; |
| Runnable resume = new Runnable() { |
| public void run() { |
| queue.add(RESUME); |
| } |
| }; |
| setSuspendResume(suspend, resume); |
| start(); |
| if (isStarted()) { |
| if (postStartAction != null) { |
| postStartAction.run(); |
| } |
| ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { |
| public void run() { |
| waitFor(); |
| queue.add(EXIT); |
| } |
| }); |
| boolean suspended = false; |
| while (true) { |
| Object action; |
| while (true) { |
| try { |
| action = queue.take(); |
| break; |
| } |
| catch (InterruptedException e) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("queue.take() is interrupted", e); |
| } |
| } |
| } |
| if (action == EXIT) { |
| if (suspended) { |
| LOG.error("Exiting while RW lock is suspended (reacquiring W-lock command)"); |
| vcs.getCommandLock().writeLock().lock(); |
| } |
| break; |
| } |
| else if (action == SUSPEND) { |
| if (suspended) { |
| LOG.error("Suspending suspended W-lock (ignoring command)"); |
| } |
| else { |
| vcs.getCommandLock().writeLock().unlock(); |
| suspended = true; |
| } |
| } |
| else if (action == RESUME) { |
| if (!suspended) { |
| LOG.error("Resuming not suspended W-lock (ignoring command)"); |
| } |
| else { |
| vcs.getCommandLock().writeLock().lock(); |
| suspended = false; |
| } |
| } |
| } |
| } |
| } |
| else { |
| start(); |
| if (isStarted()) { |
| if (postStartAction != null) { |
| postStartAction.run(); |
| } |
| waitFor(); |
| } |
| } |
| } |
| finally { |
| switch (myCommand.lockingPolicy()) { |
| case READ: |
| break; |
| case WRITE_SUSPENDABLE: |
| case WRITE: |
| vcs.getCommandLock().writeLock().unlock(); |
| break; |
| } |
| |
| logTime(); |
| } |
| } |
| |
| private void logTime() { |
| if (myStartTime > 0) { |
| long time = System.currentTimeMillis() - myStartTime; |
| if (!LOG.isDebugEnabled() && time > LONG_TIME) { |
| LOG.info(String.format("git %s took %s ms. Command parameters: %n%s", myCommand, time, myCommandLine.getCommandLineString())); |
| } |
| else { |
| LOG.debug(String.format("git %s took %s ms", myCommand, time)); |
| } |
| } |
| else { |
| LOG.debug(String.format("git %s finished.", myCommand)); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return myCommandLine.toString(); |
| } |
| } |