<commit_before>/*
 * Copyright 2000-2011 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.update;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FilePathImpl;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.LocalChangeList;
import com.intellij.openapi.vcs.changes.ui.ChangeListViewerDialog;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import git4idea.GitUtil;
import git4idea.branch.GitBranchPair;
import git4idea.commands.*;
import git4idea.merge.GitMergeConflictResolver;
import git4idea.merge.GitMerger;
import git4idea.ui.GitUIUtil;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Handles <code>git pull</code> via merge.
 */
public class GitMergeUpdater extends GitUpdater {
  private static final Logger LOG = Logger.getInstance(GitMergeUpdater.class);

  private final ChangeListManager myChangeListManager;

  public GitMergeUpdater(Project project,
                         VirtualFile root,
                         GitUpdateProcess gitUpdateProcess,
                         ProgressIndicator progressIndicator,
                         UpdatedFiles updatedFiles) {
    super(project, root, gitUpdateProcess, progressIndicator, updatedFiles);
    myChangeListManager = ChangeListManager.getInstance(myProject);
  }

  @Override
  @NotNull
  protected GitUpdateResult doUpdate() {
    LOG.info("doUpdate ");
    final GitMerger merger = new GitMerger(myProject);
    final GitLineHandler mergeHandler = new GitLineHandler(myProject, myRoot, GitCommand.MERGE);
    mergeHandler.addParameters("--no-stat", "-v");
    mergeHandler.addParameters(myUpdateProcess.getTrackedBranches().get(myRoot).getTracked().getName());

    final MergeLineListener mergeLineListener = new MergeLineListener();
    mergeHandler.addLineListener(mergeLineListener);

    final GitTask mergeTask = new GitTask(myProject, mergeHandler, "git merge");
    mergeTask.setExecuteResultInAwt(false);
    mergeTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
    final AtomicReference<GitUpdateResult> updateResult = new AtomicReference<GitUpdateResult>();
    final AtomicBoolean failure = new AtomicBoolean();
    mergeTask.executeInBackground(true, new GitTaskResultHandlerAdapter() {
      @Override protected void onSuccess() {
        updateResult.set(GitUpdateResult.SUCCESS);
      }

      @Override protected void onCancel() {
        cancel();
        updateResult.set(GitUpdateResult.CANCEL);
      }

      @Override protected void onFailure() {
        failure.set(true);
      }
    });

    if (failure.get()) {
      updateResult.set(handleMergeFailure(mergeLineListener, merger, mergeHandler));
    }
    return updateResult.get();
  }

  @NotNull
  private GitUpdateResult handleMergeFailure(MergeLineListener mergeLineListener, final GitMerger merger, GitLineHandler mergeHandler) {
    final MergeError error = mergeLineListener.getMergeError();
    LOG.info("doUpdate merge error: " + error);
    if (error == MergeError.CONFLICT) {
      final boolean allMerged =
        new GitMergeConflictResolver(myProject, false, "Merge conflicts detected. Resolve them before continuing update.",
                                     "Can't complete update", "") {
          @Override protected boolean proceedIfNothingToMerge() throws VcsException {
            merger.mergeCommit(myRoot);
            return true;
          }

          @Override protected boolean proceedAfterAllMerged() throws VcsException {
            merger.mergeCommit(myRoot);
            return true;
          }
        }.merge(Collections.singleton(myRoot));
      return allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE;
    }
    else if (error == MergeError.LOCAL_CHANGES) {
      final List<FilePath> paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput());
      final Collection<Change> changes = getLocalChangesFilteredByFiles(paths);
      final ChangeListViewerDialog dialog = new ChangeListViewerDialog(myProject, changes, false) {
        @Override protected String getDescription() {
          return "Your local changes to the following files would be overwritten by merge.<br/>" +
                            "Please, commit your changes or stash them before you can merge.";
        }
      };
      UIUtil.invokeAndWaitIfNeeded(new Runnable() {
        @Override public void run() {
          dialog.show();
        }
      });
      return GitUpdateResult.ERROR;
    } else {
      GitUIUtil.notifyImportantError(myProject, "Error merging", GitUIUtil.stringifyErrors(mergeHandler.errors()));
      return GitUpdateResult.ERROR;
    }
  }

  @Override
  public boolean isSaveNeeded() {
    try {
      if (hasStagedChanges()) {
        return true;
      }
    }
    catch (VcsException e) {
      LOG.info("isSaveNeeded failed to check staging area", e);
      return true;
    }

    // git log --name-status master..origin/master
    GitBranchPair gitBranchPair = myUpdateProcess.getTrackedBranches().get(myRoot);
    String currentBranch = gitBranchPair.getBranch().getName();
    String remoteBranch = gitBranchPair.getTracked().getName();
    try {
      Collection<String> remotelyChanged = getRemotelyChangedPaths(currentBranch, remoteBranch);
      Collection<FilePath> locallyChanged = myUpdateProcess.getSaver().getChangedFiles();
      for (FilePath localPath : locallyChanged) {
        if (remotelyChanged.contains(localPath.getPath())) { // found a file which was changed locally and remotely => need to save
          return true;
        }
      }
      return false;
    } catch (VcsException e) {
      LOG.info("failed to get remotely changed files for " + currentBranch + ".." + remoteBranch, e);
      return true; // fail safe
    }
  }

  /**
   * git diff --name-only --cached
   * @return true if there is anything in the staging area, false if the staging area is empty.
   */
  private boolean hasStagedChanges() throws VcsException {
    final GitSimpleHandler diff = new GitSimpleHandler(myProject, myRoot, GitCommand.DIFF);
    diff.addParameters("--name-only", "--cached");
    diff.setNoSSH(true);
    diff.setStdoutSuppressed(true);
    diff.setStderrSuppressed(true);
    final String output = diff.run();
    return !output.trim().isEmpty();
  }

  private void cancel() {
    try {
      GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.RESET);
      h.addParameters("-v", "--merge");
      h.run();
    } catch (VcsException e) {
      LOG.info("cancel git reset --merge", e);
      GitUIUtil.notifyImportantError(myProject, "Couldn't reset merge", e.getLocalizedMessage());
    }
  }

  // parses the output of merge conflict returning files which would be overwritten by merge. These files will be stashed.
  private List<FilePath> getFilesOverwrittenByMerge(@NotNull List<String> mergeOutput) {
    final List<FilePath> paths = new ArrayList<FilePath>();
    for  (String line : mergeOutput) {
      if (StringUtil.isEmptyOrSpaces(line)) {
        continue;<commit_msg>Git update cancel: no-ssh-exception.<commit_after>/*
 * Copyright 2000-2011 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.update;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FilePathImpl;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.LocalChangeList;
import com.intellij.openapi.vcs.changes.ui.ChangeListViewerDialog;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import git4idea.GitUtil;
import git4idea.branch.GitBranchPair;
import git4idea.commands.*;
import git4idea.merge.GitMergeConflictResolver;
import git4idea.merge.GitMerger;
import git4idea.ui.GitUIUtil;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Handles <code>git pull</code> via merge.
 */
public class GitMergeUpdater extends GitUpdater {
  private static final Logger LOG = Logger.getInstance(GitMergeUpdater.class);

  private final ChangeListManager myChangeListManager;

  public GitMergeUpdater(Project project,
                         VirtualFile root,
                         GitUpdateProcess gitUpdateProcess,
                         ProgressIndicator progressIndicator,
                         UpdatedFiles updatedFiles) {
    super(project, root, gitUpdateProcess, progressIndicator, updatedFiles);
    myChangeListManager = ChangeListManager.getInstance(myProject);
  }

  @Override
  @NotNull
  protected GitUpdateResult doUpdate() {
    LOG.info("doUpdate ");
    final GitMerger merger = new GitMerger(myProject);
    final GitLineHandler mergeHandler = new GitLineHandler(myProject, myRoot, GitCommand.MERGE);
    mergeHandler.addParameters("--no-stat", "-v");
    mergeHandler.addParameters(myUpdateProcess.getTrackedBranches().get(myRoot).getTracked().getName());

    final MergeLineListener mergeLineListener = new MergeLineListener();
    mergeHandler.addLineListener(mergeLineListener);

    final GitTask mergeTask = new GitTask(myProject, mergeHandler, "git merge");
    mergeTask.setExecuteResultInAwt(false);
    mergeTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
    final AtomicReference<GitUpdateResult> updateResult = new AtomicReference<GitUpdateResult>();
    final AtomicBoolean failure = new AtomicBoolean();
    mergeTask.executeInBackground(true, new GitTaskResultHandlerAdapter() {
      @Override protected void onSuccess() {
        updateResult.set(GitUpdateResult.SUCCESS);
      }

      @Override protected void onCancel() {
        cancel();
        updateResult.set(GitUpdateResult.CANCEL);
      }

      @Override protected void onFailure() {
        failure.set(true);
      }
    });

    if (failure.get()) {
      updateResult.set(handleMergeFailure(mergeLineListener, merger, mergeHandler));
    }
    return updateResult.get();
  }

  @NotNull
  private GitUpdateResult handleMergeFailure(MergeLineListener mergeLineListener, final GitMerger merger, GitLineHandler mergeHandler) {
    final MergeError error = mergeLineListener.getMergeError();
    LOG.info("doUpdate merge error: " + error);
    if (error == MergeError.CONFLICT) {
      final boolean allMerged =
        new GitMergeConflictResolver(myProject, false, "Merge conflicts detected. Resolve them before continuing update.",
                                     "Can't complete update", "") {
          @Override protected boolean proceedIfNothingToMerge() throws VcsException {
            merger.mergeCommit(myRoot);
            return true;
          }

          @Override protected boolean proceedAfterAllMerged() throws VcsException {
            merger.mergeCommit(myRoot);
            return true;
          }
        }.merge(Collections.singleton(myRoot));
      return allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE;
    }
    else if (error == MergeError.LOCAL_CHANGES) {
      final List<FilePath> paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput());
      final Collection<Change> changes = getLocalChangesFilteredByFiles(paths);
      final ChangeListViewerDialog dialog = new ChangeListViewerDialog(myProject, changes, false) {
        @Override protected String getDescription() {
          return "Your local changes to the following files would be overwritten by merge.<br/>" +
                            "Please, commit your changes or stash them before you can merge.";
        }
      };
      UIUtil.invokeAndWaitIfNeeded(new Runnable() {
        @Override public void run() {
          dialog.show();
        }
      });
      return GitUpdateResult.ERROR;
    } else {
      GitUIUtil.notifyImportantError(myProject, "Error merging", GitUIUtil.stringifyErrors(mergeHandler.errors()));
      return GitUpdateResult.ERROR;
    }
  }

  @Override
  public boolean isSaveNeeded() {
    try {
      if (hasStagedChanges()) {
        return true;
      }
    }
    catch (VcsException e) {
      LOG.info("isSaveNeeded failed to check staging area", e);
      return true;
    }

    // git log --name-status master..origin/master
    GitBranchPair gitBranchPair = myUpdateProcess.getTrackedBranches().get(myRoot);
    String currentBranch = gitBranchPair.getBranch().getName();
    String remoteBranch = gitBranchPair.getTracked().getName();
    try {
      Collection<String> remotelyChanged = getRemotelyChangedPaths(currentBranch, remoteBranch);
      Collection<FilePath> locallyChanged = myUpdateProcess.getSaver().getChangedFiles();
      for (FilePath localPath : locallyChanged) {
        if (remotelyChanged.contains(localPath.getPath())) { // found a file which was changed locally and remotely => need to save
          return true;
        }
      }
      return false;
    } catch (VcsException e) {
      LOG.info("failed to get remotely changed files for " + currentBranch + ".." + remoteBranch, e);
      return true; // fail safe
    }
  }

  /**
   * git diff --name-only --cached
   * @return true if there is anything in the staging area, false if the staging area is empty.
   */
  private boolean hasStagedChanges() throws VcsException {
    final GitSimpleHandler diff = new GitSimpleHandler(myProject, myRoot, GitCommand.DIFF);
    diff.addParameters("--name-only", "--cached");
    diff.setNoSSH(true);
    diff.setStdoutSuppressed(true);
    diff.setStderrSuppressed(true);
    final String output = diff.run();
    return !output.trim().isEmpty();
  }

  private void cancel() {
    try {
      GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.RESET);
      h.setNoSSH(true);
      h.addParameters("-v", "--merge");
      h.run();
    } catch (VcsException e) {
      LOG.info("cancel git reset --merge", e);
      GitUIUtil.notifyImportantError(myProject, "Couldn't reset merge", e.getLocalizedMessage());
    }
  }

  // parses the output of merge conflict returning files which would be overwritten by merge. These files will be stashed.
  private List<FilePath> getFilesOverwrittenByMerge(@NotNull List<String> mergeOutput) {
    final List<FilePath> paths = new ArrayList<FilePath>();
    for  (String line : mergeOutput) {
      if (StringUtil.isEmptyOrSpaces(line)) {
        continue;
{'fraction_non_alphanumeric': 0.09738009847425688, 'fraction_numerical': 0.0023098899762932345, 'mean_word_length': 3.528488852188274, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3312254', 'n_tokens_mistral': 4572, 'n_tokens_neox': 4492, 'n_words': 1127}