mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[git] Rewrite Git push procedure & use new VcsPushDialog
This commit is contained in:
@@ -25,13 +25,13 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class VcsNotifier {
|
||||
|
||||
private static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup(
|
||||
public static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup(
|
||||
"Vcs Messages", ChangesViewContentManager.TOOLWINDOW_ID);
|
||||
private static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup(
|
||||
public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup(
|
||||
"Vcs Important Messages", NotificationDisplayType.STICKY_BALLOON, true);
|
||||
private static final NotificationGroup MINOR_NOTIFICATION = new NotificationGroup(
|
||||
public static final NotificationGroup MINOR_NOTIFICATION = new NotificationGroup(
|
||||
"Vcs Minor Notifications", NotificationDisplayType.BALLOON, true);
|
||||
private static final NotificationGroup SILENT_NOTIFICATION = new NotificationGroup(
|
||||
public static final NotificationGroup SILENT_NOTIFICATION = new NotificationGroup(
|
||||
"Vcs Silent Notifications", NotificationDisplayType.NONE, true);
|
||||
|
||||
private final @NotNull Project myProject;
|
||||
|
||||
@@ -127,6 +127,10 @@ public class Executor {
|
||||
FileUtil.writeToFile(file, content.getBytes(), true);
|
||||
}
|
||||
|
||||
public static void append(@NotNull String fileName, @NotNull String content) throws IOException {
|
||||
append(child(fileName), content);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static File mkdir(@NotNull String dirName) {
|
||||
File file = child(dirName);
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<checkinHandlerFactory implementation="git4idea.checkin.UnresolvedMergeCheckFactory"/>
|
||||
<logProvider implementation="git4idea.log.GitLogProvider"/>
|
||||
<vcsChangesViewRefresher implementation="git4idea.changes.GitChangesViewRefresher" />
|
||||
<pushSupport implementation="git4idea.push.GitPushSupport"/>
|
||||
|
||||
<projectService
|
||||
serviceInterface="git4idea.history.wholeTree.LogFactoryService"
|
||||
|
||||
@@ -15,33 +15,65 @@
|
||||
*/
|
||||
package git4idea.actions;
|
||||
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.dvcs.branch.DvcsBranchSync;
|
||||
import com.intellij.dvcs.push.ui.VcsPushDialog;
|
||||
import com.intellij.dvcs.repo.RepositoryUtil;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
||||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import git4idea.GitPlatformFacade;
|
||||
import git4idea.push.GitPusher;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.config.GitVcsSettings;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* The action that pushes active branches
|
||||
*/
|
||||
public class GitPushAction extends GitRepositoryAction {
|
||||
public class GitPushAction extends DumbAwareAction {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
protected String getActionName() {
|
||||
return "Push";
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
Project project = e.getRequiredData(CommonDataKeys.PROJECT);
|
||||
Collection<GitRepository> repositories = collectRepositories(project, e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY));
|
||||
|
||||
new VcsPushDialog(project, RepositoryUtil.sortRepositories(repositories)).show();
|
||||
}
|
||||
|
||||
protected void perform(@NotNull final Project project,
|
||||
@NotNull final List<VirtualFile> gitRoots,
|
||||
@NotNull final VirtualFile defaultRoot,
|
||||
final Set<VirtualFile> affectedRoots,
|
||||
final List<VcsException> exceptions) throws VcsException {
|
||||
GitPusher.showPushDialogAndPerformPush(project, ServiceManager.getService(project, GitPlatformFacade.class));
|
||||
@NotNull
|
||||
private static Collection<GitRepository> collectRepositories(@NotNull Project project,
|
||||
@Nullable VirtualFile[] files) {
|
||||
GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
|
||||
Collection<GitRepository> repositories;
|
||||
if (files == null) {
|
||||
if (GitVcsSettings.getInstance(project).getSyncSetting() == DvcsBranchSync.SYNC) {
|
||||
repositories = manager.getRepositories();
|
||||
}
|
||||
else {
|
||||
repositories = Collections.singletonList(GitBranchUtil.getCurrentRepository(project));
|
||||
}
|
||||
}
|
||||
else {
|
||||
repositories = ContainerUtil.newHashSet();
|
||||
for (VirtualFile file : files) {
|
||||
GitRepository repo = manager.getRepositoryForFile(file);
|
||||
if (repo != null) {
|
||||
repositories.add(repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
return repositories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(AnActionEvent e) {
|
||||
super.update(e);
|
||||
Project project = e.getProject();
|
||||
e.getPresentation().setEnabledAndVisible(project != null && !GitUtil.getRepositoryManager(project).getRepositories().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import git4idea.GitPlatformFacade;
|
||||
import git4idea.commands.Git;
|
||||
import git4idea.commands.GitCommandResult;
|
||||
import git4idea.commands.GitCompoundResult;
|
||||
import git4idea.push.GitSimplePushResult;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.ui.branch.GitMultiRootBranchConfig;
|
||||
@@ -36,7 +35,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -173,13 +171,6 @@ class GitDeleteRemoteBranchOperation extends GitBranchOperation {
|
||||
return myGit.push(repository, remoteName, url,":" + branchName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GitCommandResult convertSimplePushResultToCommandResult(@NotNull GitSimplePushResult result) {
|
||||
boolean success = result.getType() == GitSimplePushResult.Type.SUCCESS;
|
||||
return new GitCommandResult(success, -1, success ? Collections.<String>emptyList() : Collections.singletonList(result.getOutput()),
|
||||
success ? Collections.singletonList(result.getOutput()) : Collections.<String>emptyList(), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GitRemote getRemoteByName(@NotNull GitRepository repository, @NotNull String remoteName) {
|
||||
for (GitRemote remote : repository.getRemotes()) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package git4idea.checkin;
|
||||
import com.intellij.CommonBundle;
|
||||
import com.intellij.dvcs.DvcsCommitAdditionalComponent;
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.dvcs.push.ui.VcsPushDialog;
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.editor.ex.EditorEx;
|
||||
@@ -47,7 +48,6 @@ import com.intellij.vcs.log.VcsUser;
|
||||
import com.intellij.vcs.log.VcsUserRegistry;
|
||||
import com.intellij.vcsUtil.VcsFileUtil;
|
||||
import com.intellij.vcsUtil.VcsUtil;
|
||||
import git4idea.GitPlatformFacade;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.commands.GitCommand;
|
||||
@@ -56,7 +56,7 @@ import git4idea.config.GitConfigUtil;
|
||||
import git4idea.config.GitVcsSettings;
|
||||
import git4idea.config.GitVersionSpecialty;
|
||||
import git4idea.i18n.GitBundle;
|
||||
import git4idea.push.GitPusher;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryFiles;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import git4idea.util.GitFileUtils;
|
||||
@@ -213,10 +213,12 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
|
||||
}
|
||||
}
|
||||
if (myNextCommitIsPushed != null && myNextCommitIsPushed.booleanValue() && exceptions.isEmpty()) {
|
||||
// push
|
||||
GitRepositoryManager manager = GitUtil.getRepositoryManager(myProject);
|
||||
Collection<GitRepository> repositories = GitUtil.getRepositoriesFromRoots(manager, sortedChanges.keySet());
|
||||
final List<GitRepository> preselectedRepositories = ContainerUtil.newArrayList(repositories);
|
||||
UIUtil.invokeLaterIfNeeded(new Runnable() {
|
||||
public void run() {
|
||||
GitPusher.showPushDialogAndPerformPush(myProject, ServiceManager.getService(myProject, GitPlatformFacade.class));
|
||||
new VcsPushDialog(myProject, preselectedRepositories).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import com.intellij.openapi.util.Computable;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.push.GitPushSpec;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.reset.GitResetMode;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -31,9 +32,6 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public interface Git {
|
||||
|
||||
/**
|
||||
@@ -104,16 +102,20 @@ public interface Git {
|
||||
GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
|
||||
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
|
||||
boolean updateTracking, @NotNull GitLineHandlerListener... listeners);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
|
||||
GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
|
||||
@NotNull GitLineHandlerListener... listeners);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec spec, @NotNull String url,
|
||||
@NotNull GitLineHandlerListener... listeners);
|
||||
GitCommandResult push(GitRepository repository,
|
||||
GitLocalBranch source,
|
||||
GitRemoteBranch target,
|
||||
boolean force,
|
||||
boolean updateTracking,
|
||||
GitLineHandlerListener... listeners);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult show(@NotNull GitRepository repository, @NotNull String... params);
|
||||
|
||||
@@ -23,13 +23,9 @@ import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.vcsUtil.VcsFileUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.GitExecutionException;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.*;
|
||||
import git4idea.config.GitVersionSpecialty;
|
||||
import git4idea.history.GitHistoryUtils;
|
||||
import git4idea.push.GitPushSpec;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.reset.GitResetMode;
|
||||
@@ -377,23 +373,35 @@ public class GitImpl implements Git {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public GitCommandResult push(@NotNull final GitRepository repository, @NotNull final String remote, @NotNull final String url,
|
||||
@NotNull final String spec, final boolean updateTracking,
|
||||
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
|
||||
boolean updateTracking, @NotNull GitLineHandlerListener... listeners) {
|
||||
return doPush(repository, remote, url, spec, false, updateTracking, listeners);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitCommandResult doPush(@NotNull final GitRepository repository, @NotNull final String remote, @Nullable final String url,
|
||||
@NotNull final String spec, final boolean force, final boolean updateTracking,
|
||||
@NotNull final GitLineHandlerListener... listeners) {
|
||||
return runCommand(new Computable<GitLineHandler>() {
|
||||
@Override
|
||||
public GitLineHandler compute() {
|
||||
final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
|
||||
h.setUrl(url);
|
||||
if (url != null) {
|
||||
h.setUrl(url);
|
||||
}
|
||||
h.setSilent(false);
|
||||
h.setStdoutSuppressed(false);
|
||||
addListeners(h, listeners);
|
||||
h.addProgressParameter();
|
||||
h.addParameters("--porcelain");
|
||||
h.addParameters(remote);
|
||||
h.addParameters(spec);
|
||||
if (updateTracking) {
|
||||
h.addParameters("--set-upstream");
|
||||
}
|
||||
if (force) {
|
||||
h.addParameters("--force");
|
||||
}
|
||||
return h;
|
||||
}
|
||||
});
|
||||
@@ -401,19 +409,27 @@ public class GitImpl implements Git {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String url, @NotNull String spec,
|
||||
public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
|
||||
@NotNull GitLineHandlerListener... listeners) {
|
||||
return push(repository, remote, url, spec, false, listeners);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull String url,
|
||||
@NotNull GitLineHandlerListener... listeners) {
|
||||
GitRemote remote = pushSpec.getRemote();
|
||||
GitBranch remoteBranch = pushSpec.getDest();
|
||||
String destination = remoteBranch.getName().replaceFirst(remote.getName() + "/", "");
|
||||
return push(repository, remote.getName(), url, pushSpec.getSource().getName() + ":" + destination, listeners);
|
||||
public GitCommandResult push(GitRepository repository, GitLocalBranch source, GitRemoteBranch target,
|
||||
boolean force, boolean updateTracking, GitLineHandlerListener... listeners) {
|
||||
GitRemote remote = target.getRemote();
|
||||
Collection<String> pushUrls = remote.getPushUrls(); // TODO handle the case with multiple pushurls with different protocols
|
||||
String url;
|
||||
if (pushUrls.isEmpty()) {
|
||||
LOG.error("No urls or pushUrls are defined for " + remote);
|
||||
url = null;
|
||||
}
|
||||
else {
|
||||
url = pushUrls.iterator().next();
|
||||
}
|
||||
String spec = source.getFullName() + ":" + target.getNameForRemoteOperations();
|
||||
return doPush(repository, remote.getName(), url, spec, force, updateTracking, listeners);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitCommit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds Git commits made in a single repository grouped by branches.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
final class GitCommitsByBranch {
|
||||
|
||||
private final Map<GitBranch, GitPushBranchInfo> myCommitsByBranch;
|
||||
|
||||
GitCommitsByBranch(@NotNull Map<GitBranch, GitPushBranchInfo> commitsByBranch) {
|
||||
myCommitsByBranch = new HashMap<GitBranch, GitPushBranchInfo>(commitsByBranch);
|
||||
}
|
||||
|
||||
GitCommitsByBranch(GitCommitsByBranch commitsByBranch) {
|
||||
this(new HashMap<GitBranch, GitPushBranchInfo>(commitsByBranch.myCommitsByBranch));
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
for (GitPushBranchInfo info : myCommitsByBranch.values()) {
|
||||
if (!info.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int commitsNumber() {
|
||||
int sum = 0;
|
||||
for (GitPushBranchInfo branchInfo : myCommitsByBranch.values()) {
|
||||
sum += branchInfo.getCommits().size();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Collection<GitBranch> getBranches() {
|
||||
return new HashSet<GitBranch>(myCommitsByBranch.keySet());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitPushBranchInfo get(@NotNull GitBranch branch) {
|
||||
return new GitPushBranchInfo(myCommitsByBranch.get(branch));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new GitCommitsByBranch that contains commits only from the given branch (or nothing, if the given branch didn't exist in
|
||||
* the original structure).
|
||||
*/
|
||||
@NotNull
|
||||
GitCommitsByBranch retain(@NotNull GitBranch branch) {
|
||||
Map<GitBranch, GitPushBranchInfo> res = new HashMap<GitBranch, GitPushBranchInfo>();
|
||||
if (myCommitsByBranch.containsKey(branch)) {
|
||||
res.put(branch, myCommitsByBranch.get(branch));
|
||||
}
|
||||
return new GitCommitsByBranch(res);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<GitCommit> getAllCommits() {
|
||||
Collection<GitCommit> commits = new ArrayList<GitCommit>();
|
||||
for (GitPushBranchInfo branchInfo : myCommitsByBranch.values()) {
|
||||
commits.addAll(branchInfo.getCommits());
|
||||
}
|
||||
return commits;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds Git commits grouped by repositories and by branches.
|
||||
* Actually, it is just a map (of maps of lists of commits) encapsulated in a separated class with some handy methods.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
final class GitCommitsByRepoAndBranch {
|
||||
|
||||
private final Map<GitRepository, GitCommitsByBranch> myCommitsByRepository;
|
||||
|
||||
GitCommitsByRepoAndBranch(@NotNull Map<GitRepository, GitCommitsByBranch> commitsByRepository) {
|
||||
myCommitsByRepository = commitsByRepository;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitCommitsByRepoAndBranch empty() {
|
||||
return new GitCommitsByRepoAndBranch(new HashMap<GitRepository, GitCommitsByBranch>());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Collection<GitRepository> getRepositories() {
|
||||
return new HashSet<GitRepository>(myCommitsByRepository.keySet());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitCommitsByBranch get(@NotNull GitRepository repository) {
|
||||
return new GitCommitsByBranch(myCommitsByRepository.get(repository));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new GitCommitByRepoAndBranch structure with only those repositories, which exist in the specified collection.
|
||||
*/
|
||||
@NotNull
|
||||
GitCommitsByRepoAndBranch retainAll(@NotNull Collection<GitRepository> repositories) {
|
||||
Map<GitRepository, GitCommitsByBranch> commits = new HashMap<GitRepository, GitCommitsByBranch>();
|
||||
for (GitRepository selectedRepository : repositories) {
|
||||
GitCommitsByBranch value = myCommitsByRepository.get(selectedRepository);
|
||||
if (value != null) {
|
||||
commits.put(selectedRepository, value);
|
||||
}
|
||||
}
|
||||
return new GitCommitsByRepoAndBranch(commits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new GitCommitByRepoAndBranch structure with only those pairs repository-branch, which exist in the specified map.
|
||||
*/
|
||||
@NotNull
|
||||
GitCommitsByRepoAndBranch retainAll(@NotNull Map<GitRepository, GitBranch> repositoriesBranches) {
|
||||
Map<GitRepository, GitCommitsByBranch> commits = new HashMap<GitRepository, GitCommitsByBranch>();
|
||||
for (GitRepository repository : repositoriesBranches.keySet()) {
|
||||
GitCommitsByBranch commitsByBranch = myCommitsByRepository.get(repository);
|
||||
if (commitsByBranch != null) {
|
||||
commits.put(repository, commitsByBranch.retain(repositoriesBranches.get(repository)));
|
||||
}
|
||||
}
|
||||
return new GitCommitsByRepoAndBranch(commits);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<GitCommit> getAllCommits() {
|
||||
Collection<GitCommit> commits = new ArrayList<GitCommit>();
|
||||
for (GitCommitsByBranch commitsByBranch : myCommitsByRepository.values()) {
|
||||
commits.addAll(commitsByBranch.getAllCommits());
|
||||
}
|
||||
return commits;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2012 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.push;
|
||||
|
||||
import com.intellij.icons.AllIcons;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.actionSystem.impl.ActionButton;
|
||||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.ui.ListCellRendererWrapper;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.util.ui.GridBag;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
class GitManualPushToBranch extends JPanel {
|
||||
|
||||
private final Collection<GitRepository> myRepositories;
|
||||
|
||||
private final JCheckBox myManualPush;
|
||||
private final JTextField myDestBranchTextField;
|
||||
private final JBLabel myComment;
|
||||
private final GitPushLogRefreshAction myRefreshAction;
|
||||
private final JComponent myRefreshButton;
|
||||
private final RemoteSelector myRemoteSelector;
|
||||
private final JComponent myRemoteSelectorComponent;
|
||||
|
||||
GitManualPushToBranch(@NotNull Collection<GitRepository> repositories,
|
||||
@NotNull final Runnable performOnRefresh) {
|
||||
super();
|
||||
myRepositories = repositories;
|
||||
|
||||
myManualPush = new JCheckBox("Push current branch to alternative branch: ", false);
|
||||
myManualPush.setMnemonic('b');
|
||||
|
||||
myDestBranchTextField = new JTextField(20);
|
||||
|
||||
myComment = new JBLabel("This will apply to all selected repositories", UIUtil.ComponentStyle.SMALL);
|
||||
|
||||
myRefreshAction = new GitPushLogRefreshAction() {
|
||||
@Override public void actionPerformed(AnActionEvent e) {
|
||||
performOnRefresh.run();
|
||||
}
|
||||
};
|
||||
myRefreshButton = new ActionButton(myRefreshAction, myRefreshAction.getTemplatePresentation(), myRefreshAction.getTemplatePresentation().getText(), ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
|
||||
myRefreshButton.setFocusable(true);
|
||||
final ShortcutSet shortcutSet = ActionManager.getInstance().getAction(IdeActions.ACTION_REFRESH).getShortcutSet();
|
||||
myRefreshAction.registerCustomShortcutSet(shortcutSet, myRefreshButton);
|
||||
|
||||
myRemoteSelector = new RemoteSelector(getRemotesWithCommonNames(repositories));
|
||||
myRemoteSelectorComponent = myRemoteSelector.createComponent();
|
||||
|
||||
setDefaultComponentsEnabledState(myManualPush.isSelected());
|
||||
myManualPush.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
boolean isManualPushSelected = myManualPush.isSelected();
|
||||
setDefaultComponentsEnabledState(isManualPushSelected);
|
||||
if (isManualPushSelected) {
|
||||
myDestBranchTextField.requestFocus();
|
||||
myDestBranchTextField.selectAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
layoutComponents();
|
||||
}
|
||||
|
||||
private void setDefaultComponentsEnabledState(boolean selected) {
|
||||
myDestBranchTextField.setEnabled(selected);
|
||||
myComment.setEnabled(selected);
|
||||
myRemoteSelector.setEnabled(selected);
|
||||
}
|
||||
|
||||
private void layoutComponents() {
|
||||
JPanel panel = new JPanel();
|
||||
GridBagLayout layout = new GridBagLayout();
|
||||
panel.setLayout(layout);
|
||||
GridBag g = new GridBag()
|
||||
.setDefaultFill(GridBagConstraints.NONE)
|
||||
.setDefaultAnchor(GridBagConstraints.BASELINE_LEADING)
|
||||
.setDefaultWeightX(1, 1)
|
||||
.setDefaultInsets(new Insets(0, 0, UIUtil.DEFAULT_VGAP, 5))
|
||||
;
|
||||
|
||||
panel.add(myManualPush, g.nextLine().next());
|
||||
panel.add(myRemoteSelectorComponent, g.next());
|
||||
panel.add(myDestBranchTextField, g.next());
|
||||
panel.add(myRefreshButton, g.next());
|
||||
if (myRepositories.size() > 1) {
|
||||
panel.add(myComment, g.nextLine().insets(0, 28, 0, 0).coverLine());
|
||||
}
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
add(panel, BorderLayout.WEST);
|
||||
}
|
||||
|
||||
boolean turnedOn() {
|
||||
return myManualPush.isSelected() && !myDestBranchTextField.getText().isEmpty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getBranchToPush() {
|
||||
return myDestBranchTextField.getText();
|
||||
}
|
||||
|
||||
void setBranchToPushIfNotSet(@NotNull String text) {
|
||||
if (myDestBranchTextField.getText().isEmpty()) {
|
||||
myDestBranchTextField.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitRemote getSelectedRemote() {
|
||||
return myRemoteSelector.getSelectedValue();
|
||||
}
|
||||
|
||||
public void selectRemote(String remoteName) {
|
||||
myRemoteSelector.selectRemote(remoteName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Collection<GitRemote> getRemotesWithCommonNames(@NotNull Collection<GitRepository> repositories) {
|
||||
if (repositories.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Iterator<GitRepository> iterator = repositories.iterator();
|
||||
List<GitRemote> commonRemotes = new ArrayList<GitRemote>(iterator.next().getRemotes());
|
||||
while (iterator.hasNext()) {
|
||||
GitRepository repository = iterator.next();
|
||||
Collection<String> remoteNames = getRemoteNames(repository);
|
||||
for (Iterator<GitRemote> commonIter = commonRemotes.iterator(); commonIter.hasNext(); ) {
|
||||
GitRemote remote = commonIter.next();
|
||||
if (!remoteNames.contains(remote.getName())) {
|
||||
commonIter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return commonRemotes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Collection<String> getRemoteNames(@NotNull GitRepository repository) {
|
||||
Collection<String> names = new ArrayList<String>(repository.getRemotes().size());
|
||||
for (GitRemote remote : repository.getRemotes()) {
|
||||
names.add(remote.getName());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to select remotes.
|
||||
* Just a JCombobox actually, but more flexible: if there is only one remote, we could use JLabel or something like that.
|
||||
*/
|
||||
private static class RemoteSelector {
|
||||
|
||||
private final Collection<GitRemote> myRemotes;
|
||||
private JComboBox myRemoteCombobox;
|
||||
|
||||
private RemoteSelector(@NotNull Collection<GitRemote> remotes) {
|
||||
myRemotes = remotes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
JComponent createComponent() {
|
||||
myRemoteCombobox = new JComboBox();
|
||||
myRemoteCombobox.setRenderer(new RemoteCellRenderer(myRemoteCombobox.getRenderer()));
|
||||
for (GitRemote remote : myRemotes) {
|
||||
myRemoteCombobox.addItem(remote);
|
||||
}
|
||||
myRemoteCombobox.setToolTipText("Select remote");
|
||||
if (myRemotes.size() == 1) {
|
||||
myRemoteCombobox.setEnabled(false);
|
||||
}
|
||||
return myRemoteCombobox;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitRemote getSelectedValue() {
|
||||
return (GitRemote)myRemoteCombobox.getSelectedItem();
|
||||
}
|
||||
|
||||
void setEnabled(boolean selected) {
|
||||
if (myRemotes.size() > 1) {
|
||||
myRemoteCombobox.setEnabled(selected);
|
||||
}
|
||||
}
|
||||
|
||||
public void selectRemote(@NotNull String remoteName) {
|
||||
for (GitRemote remote : myRemotes) {
|
||||
if (remote.getName().equals(remoteName)) {
|
||||
myRemoteCombobox.setSelectedItem(remote);
|
||||
return;
|
||||
}
|
||||
}
|
||||
myRemoteCombobox.setSelectedIndex(0);
|
||||
}
|
||||
|
||||
private static class RemoteCellRenderer extends ListCellRendererWrapper {
|
||||
public RemoteCellRenderer(final ListCellRenderer listCellRenderer) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) {
|
||||
if (value instanceof GitRemote) {
|
||||
setText(((GitRemote)value).getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class GitPushLogRefreshAction extends DumbAwareAction {
|
||||
|
||||
GitPushLogRefreshAction() {
|
||||
super("Refresh commit list", "Refresh commit list", AllIcons.Actions.Refresh);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.*;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.vcs.log.VcsFullCommitDetails;
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.history.GitHistoryUtils;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class GitOutgoingCommitsProvider extends OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> {
|
||||
|
||||
@NotNull private final Project myProject;
|
||||
|
||||
public GitOutgoingCommitsProvider(@NotNull Project project) {
|
||||
myProject = project;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public OutgoingResult getOutgoingCommits(@NotNull GitRepository repository, @NotNull PushSpec<GitPushSource, GitPushTarget> pushSpec,
|
||||
boolean initial) {
|
||||
String source = pushSpec.getSource().getBranch().getFullName();
|
||||
GitPushTarget target = pushSpec.getTarget();
|
||||
assert target != null;
|
||||
String destination = target.getBranch().getFullName();
|
||||
try {
|
||||
List<GitCommit> commits;
|
||||
if (!target.isNewBranchCreated()) {
|
||||
commits = GitHistoryUtils.history(myProject, repository.getRoot(), destination + ".." + source);
|
||||
}
|
||||
else {
|
||||
commits = GitHistoryUtils.history(myProject, repository.getRoot(),
|
||||
source, "--not", "--remotes=" + target.getBranch().getRemote().getName(), "--max-count=" + 1000);
|
||||
}
|
||||
return new OutgoingResult(commits, Collections.<VcsError>emptyList());
|
||||
}
|
||||
catch (VcsException e) {
|
||||
return new OutgoingResult(Collections.<VcsFullCommitDetails>emptyList(),
|
||||
Collections.singletonList(new VcsError(GitUtil.cleanupErrorPrefixes(e.getMessage()))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Holds the list of commits and the destination branch (which a branch associated with this GitPushBranchInfo is going to be pushed to).
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
final class GitPushBranchInfo {
|
||||
|
||||
private final GitLocalBranch mySourceBranch;
|
||||
private final GitRemoteBranch myDestBranch;
|
||||
private final Type myType;
|
||||
private final List<GitCommit> myCommits;
|
||||
|
||||
enum Type {
|
||||
STANDARD, // the branch this branch is targeted, exists (and is either the tracked/matched branch or manually specified)
|
||||
NEW_BRANCH, // the source branch will be pushed to a new branch
|
||||
NO_TRACKED_OR_TARGET // the branch has no tracked/matched, and target was not manually specified
|
||||
}
|
||||
|
||||
GitPushBranchInfo(@NotNull GitLocalBranch sourceBranch, @NotNull GitRemoteBranch destBranch,
|
||||
@NotNull List<GitCommit> commits, @NotNull Type type) {
|
||||
mySourceBranch = sourceBranch;
|
||||
myCommits = commits;
|
||||
myDestBranch = destBranch;
|
||||
myType = type;
|
||||
}
|
||||
|
||||
GitPushBranchInfo(@NotNull GitPushBranchInfo pushBranchInfo) {
|
||||
this(pushBranchInfo.getSourceBranch(), pushBranchInfo.getDestBranch(), pushBranchInfo.getCommits(), pushBranchInfo.getType());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Type getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
boolean isNewBranchCreated() {
|
||||
return myType == Type.NEW_BRANCH;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitRemoteBranch getDestBranch() {
|
||||
return myDestBranch;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
List<GitCommit> getCommits() {
|
||||
return new ArrayList<GitCommit>(myCommits);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitLocalBranch getSourceBranch() {
|
||||
return mySourceBranch;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return myCommits.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Result of pushing a single branch.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
final class GitPushBranchResult {
|
||||
|
||||
private final Type myType;
|
||||
private final int myNumberOfPushedCommits;
|
||||
private final String myTargetBranchName;
|
||||
|
||||
enum Type {
|
||||
SUCCESS,
|
||||
NEW_BRANCH,
|
||||
REJECTED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private GitPushBranchResult(Type type, int numberOfPushedCommits, @Nullable String targetBranchName) {
|
||||
myType = type;
|
||||
myNumberOfPushedCommits = numberOfPushedCommits;
|
||||
myTargetBranchName = targetBranchName;
|
||||
}
|
||||
|
||||
static GitPushBranchResult success(int numberOfPushedCommits) {
|
||||
return new GitPushBranchResult(Type.SUCCESS, numberOfPushedCommits, null);
|
||||
}
|
||||
|
||||
static GitPushBranchResult newBranch(String targetBranchName) {
|
||||
return new GitPushBranchResult(Type.NEW_BRANCH, 0, targetBranchName);
|
||||
}
|
||||
|
||||
static GitPushBranchResult rejected() {
|
||||
return new GitPushBranchResult(Type.REJECTED, 0, null);
|
||||
}
|
||||
|
||||
static GitPushBranchResult error() {
|
||||
return new GitPushBranchResult(Type.ERROR, 0, null);
|
||||
}
|
||||
|
||||
int getNumberOfPushedCommits() {
|
||||
return myNumberOfPushedCommits;
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
return myType == Type.SUCCESS;
|
||||
}
|
||||
|
||||
boolean isRejected() {
|
||||
return myType == Type.REJECTED;
|
||||
}
|
||||
|
||||
boolean isError() {
|
||||
return myType == Type.ERROR;
|
||||
}
|
||||
|
||||
boolean isNewBranch() {
|
||||
return myType == Type.NEW_BRANCH;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getTargetBranchName() {
|
||||
return myTargetBranchName != null ? myTargetBranchName : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.openapi.util.Couple;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.impl.VcsGlobalMessageManager;
|
||||
import com.intellij.ui.components.JBLoadingPanel;
|
||||
import com.intellij.util.Consumer;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import git4idea.*;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitPushDialog extends DialogWrapper {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushDialog.class);
|
||||
private static final String DEFAULT_REMOTE = "origin";
|
||||
|
||||
private Project myProject;
|
||||
private final GitRepositoryManager myRepositoryManager;
|
||||
private final GitPusher myPusher;
|
||||
private final GitPushLog myListPanel;
|
||||
private GitCommitsByRepoAndBranch myGitCommitsToPush;
|
||||
private Map<GitRepository, GitPushSpec> myPushSpecs;
|
||||
private final Collection<GitRepository> myRepositories;
|
||||
private final JBLoadingPanel myLoadingPanel;
|
||||
private final Object COMMITS_LOADING_LOCK = new Object();
|
||||
private final GitManualPushToBranch myRefspecPanel;
|
||||
private final AtomicReference<String> myDestBranchInfoOnRefresh = new AtomicReference<String>();
|
||||
|
||||
private final boolean myPushPossible;
|
||||
|
||||
public GitPushDialog(@NotNull Project project) {
|
||||
super(project);
|
||||
myProject = project;
|
||||
myPusher = new GitPusher(myProject, ServiceManager.getService(project, GitPlatformFacade.class), new EmptyProgressIndicator());
|
||||
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
|
||||
|
||||
myRepositories = getRepositoriesWithRemotes();
|
||||
|
||||
myLoadingPanel = new JBLoadingPanel(new BorderLayout(), this.getDisposable());
|
||||
|
||||
myListPanel = new GitPushLog(myProject, myRepositories, new RepositoryCheckboxListener());
|
||||
myRefspecPanel = new GitManualPushToBranch(myRepositories, new RefreshButtonListener());
|
||||
|
||||
if (GitManualPushToBranch.getRemotesWithCommonNames(myRepositories).isEmpty()) {
|
||||
myRefspecPanel.setVisible(false);
|
||||
setErrorText("Can't push, because no remotes are defined");
|
||||
setOKActionEnabled(false);
|
||||
myPushPossible = false;
|
||||
} else {
|
||||
myPushPossible = true;
|
||||
}
|
||||
|
||||
init();
|
||||
setOKButtonText("Push");
|
||||
setOKButtonMnemonic('P');
|
||||
setTitle("Git Push");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<GitRepository> getRepositoriesWithRemotes() {
|
||||
List<GitRepository> repositories = new ArrayList<GitRepository>();
|
||||
for (GitRepository repository : myRepositoryManager.getRepositories()) {
|
||||
if (!repository.getRemotes().isEmpty()) {
|
||||
repositories.add(repository);
|
||||
}
|
||||
}
|
||||
return repositories;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected JComponent createNorthPanel() {
|
||||
final JComponent banner = VcsGlobalMessageManager.getInstance(myProject).getMessageBanner();
|
||||
return banner != null ? banner : super.createNorthPanel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JComponent createCenterPanel() {
|
||||
JPanel optionsPanel = new JPanel(new BorderLayout());
|
||||
optionsPanel.add(myRefspecPanel);
|
||||
|
||||
JComponent rootPanel = new JPanel(new BorderLayout(0, 15));
|
||||
rootPanel.add(createCommitListPanel(), BorderLayout.CENTER);
|
||||
rootPanel.add(optionsPanel, BorderLayout.SOUTH);
|
||||
return rootPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getHelpId() {
|
||||
return "reference.VersionControl.Git.PushDialog";
|
||||
}
|
||||
|
||||
private JComponent createCommitListPanel() {
|
||||
myLoadingPanel.add(myListPanel, BorderLayout.CENTER);
|
||||
if (myPushPossible) {
|
||||
loadCommitsInBackground();
|
||||
} else {
|
||||
myLoadingPanel.startLoading();
|
||||
myLoadingPanel.stopLoading();
|
||||
}
|
||||
|
||||
JPanel commitListPanel = new JPanel(new BorderLayout());
|
||||
commitListPanel.add(myLoadingPanel, BorderLayout.CENTER);
|
||||
return commitListPanel;
|
||||
}
|
||||
|
||||
private void loadCommitsInBackground() {
|
||||
myLoadingPanel.startLoading();
|
||||
|
||||
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
|
||||
public void run() {
|
||||
final AtomicReference<String> error = new AtomicReference<String>();
|
||||
synchronized (COMMITS_LOADING_LOCK) {
|
||||
error.set(collectInfoToPush());
|
||||
}
|
||||
|
||||
final Couple<String> remoteAndBranch = getRemoteAndTrackedBranchForCurrentBranch();
|
||||
UIUtil.invokeLaterIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (error.get() != null) {
|
||||
myListPanel.displayError(error.get());
|
||||
} else {
|
||||
myListPanel.setCommits(myGitCommitsToPush);
|
||||
}
|
||||
if (!myRefspecPanel.turnedOn()) {
|
||||
myRefspecPanel.selectRemote(remoteAndBranch.getFirst());
|
||||
myRefspecPanel.setBranchToPushIfNotSet(remoteAndBranch.getSecond());
|
||||
}
|
||||
myLoadingPanel.stopLoading();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Couple<String> getRemoteAndTrackedBranchForCurrentBranch() {
|
||||
if (myGitCommitsToPush != null) {
|
||||
Collection<GitRepository> repositories = myGitCommitsToPush.getRepositories();
|
||||
if (!repositories.isEmpty()) {
|
||||
GitRepository repository = repositories.iterator().next();
|
||||
GitBranch currentBranch = repository.getCurrentBranch();
|
||||
assert currentBranch != null;
|
||||
if (myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch() == GitPusher.NO_TARGET_BRANCH) { // push to branch with the same name
|
||||
return Couple.of(DEFAULT_REMOTE, currentBranch.getName());
|
||||
}
|
||||
String remoteName;
|
||||
try {
|
||||
remoteName = GitBranchUtil.getTrackedRemoteName(myProject, repository.getRoot(), currentBranch.getName());
|
||||
if (remoteName == null) {
|
||||
remoteName = DEFAULT_REMOTE;
|
||||
}
|
||||
}
|
||||
catch (VcsException e) {
|
||||
LOG.info("Couldn't retrieve tracked branch for current branch " + currentBranch, e);
|
||||
remoteName = DEFAULT_REMOTE;
|
||||
}
|
||||
String targetBranch = myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch().getNameForRemoteOperations();
|
||||
return Couple.of(remoteName, targetBranch);
|
||||
}
|
||||
}
|
||||
return Couple.of(DEFAULT_REMOTE, "");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String collectInfoToPush() {
|
||||
try {
|
||||
LOG.info("collectInfoToPush...");
|
||||
myPushSpecs = pushSpecsForCurrentOrEnteredBranches();
|
||||
myGitCommitsToPush = myPusher.collectCommitsToPush(myPushSpecs);
|
||||
LOG.info(String.format("collectInfoToPush | Collected commits to push. Push spec: %s, commits: %s",
|
||||
myPushSpecs, logMessageForCommits(myGitCommitsToPush)));
|
||||
return null;
|
||||
}
|
||||
catch (VcsException e) {
|
||||
myGitCommitsToPush = GitCommitsByRepoAndBranch.empty();
|
||||
LOG.error("collectInfoToPush | Couldn't collect commits to push. Push spec: " + myPushSpecs, e);
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private static String logMessageForCommits(GitCommitsByRepoAndBranch commitsToPush) {
|
||||
StringBuilder logMessage = new StringBuilder();
|
||||
for (GitCommit commit : commitsToPush.getAllCommits()) {
|
||||
logMessage.append(DvcsUtil.getShortHash(commit.getId().toString()));
|
||||
}
|
||||
return logMessage.toString();
|
||||
}
|
||||
|
||||
private Map<GitRepository, GitPushSpec> pushSpecsForCurrentOrEnteredBranches() throws VcsException {
|
||||
Map<GitRepository, GitPushSpec> defaultSpecs = new HashMap<GitRepository, GitPushSpec>();
|
||||
for (GitRepository repository : myRepositories) {
|
||||
GitLocalBranch currentBranch = repository.getCurrentBranch();
|
||||
if (currentBranch == null) {
|
||||
continue;
|
||||
}
|
||||
String remoteName = GitBranchUtil.getTrackedRemoteName(repository.getProject(), repository.getRoot(), currentBranch.getName());
|
||||
String trackedBranchName = GitBranchUtil.getTrackedBranchName(repository.getProject(), repository.getRoot(), currentBranch.getName());
|
||||
GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
|
||||
GitRemoteBranch targetBranch;
|
||||
if (remote != null && trackedBranchName != null) {
|
||||
targetBranch = GitBranchUtil.findRemoteBranchByName(trackedBranchName, remote.getName(),
|
||||
repository.getBranches().getRemoteBranches());
|
||||
}
|
||||
else {
|
||||
Pair<GitRemote, GitRemoteBranch> remoteAndBranch = GitUtil.findMatchingRemoteBranch(repository, currentBranch);
|
||||
if (remoteAndBranch == null) {
|
||||
targetBranch = GitPusher.NO_TARGET_BRANCH;
|
||||
} else {
|
||||
targetBranch = remoteAndBranch.getSecond();
|
||||
}
|
||||
}
|
||||
|
||||
if (myRefspecPanel.turnedOn()) {
|
||||
String manualBranchName = myRefspecPanel.getBranchToPush();
|
||||
remote = myRefspecPanel.getSelectedRemote();
|
||||
GitRemoteBranch manualBranch = GitBranchUtil.findRemoteBranchByName(manualBranchName, remote.getName(),
|
||||
repository.getBranches().getRemoteBranches());
|
||||
if (manualBranch == null) {
|
||||
manualBranch = new GitStandardRemoteBranch(remote, manualBranchName, GitBranch.DUMMY_HASH);
|
||||
}
|
||||
targetBranch = manualBranch;
|
||||
}
|
||||
|
||||
GitPushSpec pushSpec = new GitPushSpec(currentBranch, targetBranch == null ? GitPusher.NO_TARGET_BRANCH : targetBranch);
|
||||
defaultSpecs.put(repository, pushSpec);
|
||||
}
|
||||
return defaultSpecs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getPreferredFocusedComponent() {
|
||||
return myListPanel.getPreferredFocusComponent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDimensionServiceKey() {
|
||||
return GitPushDialog.class.getName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitPushInfo getPushInfo() {
|
||||
// waiting for commit list loading, because this information is needed to correctly handle rejected push situation and correctly
|
||||
// notify about pushed commits
|
||||
// TODO optimize: don't refresh: information about pushed commits can be achieved from the successful push output
|
||||
LOG.info("getPushInfo start");
|
||||
synchronized (COMMITS_LOADING_LOCK) {
|
||||
GitCommitsByRepoAndBranch selectedCommits;
|
||||
if (myGitCommitsToPush == null) {
|
||||
LOG.info("getPushInfo | myGitCommitsToPush == null. collecting...");
|
||||
collectInfoToPush();
|
||||
selectedCommits = myGitCommitsToPush;
|
||||
}
|
||||
else {
|
||||
if (refreshNeeded()) {
|
||||
LOG.info("getPushInfo | refresh is needed, collecting...");
|
||||
collectInfoToPush();
|
||||
}
|
||||
Collection<GitRepository> selectedRepositories = myListPanel.getSelectedRepositories();
|
||||
selectedCommits = myGitCommitsToPush.retainAll(selectedRepositories);
|
||||
}
|
||||
LOG.info("getPushInfo | selectedCommits: " + logMessageForCommits(selectedCommits));
|
||||
return new GitPushInfo(selectedCommits, myPushSpecs);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean refreshNeeded() {
|
||||
String currentDestBranchValue = myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null;
|
||||
String savedValue = myDestBranchInfoOnRefresh.get();
|
||||
if (savedValue == null) {
|
||||
return currentDestBranchValue != null;
|
||||
}
|
||||
return !savedValue.equals(currentDestBranchValue);
|
||||
}
|
||||
|
||||
private class RepositoryCheckboxListener implements Consumer<Boolean> {
|
||||
@Override public void consume(Boolean checked) {
|
||||
if (checked) {
|
||||
setOKActionEnabled(true);
|
||||
} else {
|
||||
Collection<GitRepository> repositories = myListPanel.getSelectedRepositories();
|
||||
if (repositories.isEmpty()) {
|
||||
setOKActionEnabled(false);
|
||||
} else {
|
||||
setOKActionEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RefreshButtonListener implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
myDestBranchInfoOnRefresh.set(myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null);
|
||||
loadCommitsInBackground();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import com.intellij.util.containers.HashMap;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Holds information about a single Git push action which is to be executed or has been executed.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public final class GitPushInfo {
|
||||
|
||||
@NotNull private final GitCommitsByRepoAndBranch myCommits;
|
||||
@NotNull private final Map<GitRepository, GitPushSpec> myPushSpecs;
|
||||
|
||||
/**
|
||||
* We pass the complex {@link GitCommitsByRepoAndBranch} structure here instead of just the list of repositories,
|
||||
* because later (after successful push, for example) it may be needed for showing useful notifications, such as number of commits pushed.
|
||||
*/
|
||||
public GitPushInfo(@NotNull GitCommitsByRepoAndBranch commits, @NotNull Map<GitRepository, GitPushSpec> pushSpecs) {
|
||||
myCommits = commits;
|
||||
myPushSpecs = pushSpecs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<GitRepository, GitPushSpec> getPushSpecs() {
|
||||
return myPushSpecs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitCommitsByRepoAndBranch getCommits() {
|
||||
return myCommits;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitPushInfo retain(Map<GitRepository, GitBranch> repoBranchMap) {
|
||||
return new GitPushInfo(myCommits.retainAll(repoBranchMap), new HashMap<GitRepository, GitPushSpec>(myPushSpecs));
|
||||
}
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.openapi.actionSystem.CommonShortcuts;
|
||||
import com.intellij.openapi.actionSystem.DataKey;
|
||||
import com.intellij.openapi.actionSystem.DataSink;
|
||||
import com.intellij.openapi.actionSystem.TypeSafeDataProvider;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.Splitter;
|
||||
import com.intellij.openapi.vcs.VcsDataKeys;
|
||||
import com.intellij.openapi.vcs.changes.Change;
|
||||
import com.intellij.openapi.vcs.changes.issueLinks.IssueLinkHtmlRenderer;
|
||||
import com.intellij.openapi.vcs.changes.ui.ChangesBrowser;
|
||||
import com.intellij.ui.*;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.Consumer;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import com.intellij.util.ui.tree.TreeUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitCommit;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.TreeSelectionEvent;
|
||||
import javax.swing.event.TreeSelectionListener;
|
||||
import javax.swing.tree.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* The list of commits from multiple repositories and branches, with diff panel at the right.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
class GitPushLog extends JPanel implements TypeSafeDataProvider {
|
||||
|
||||
private final Project myProject;
|
||||
private final Collection<GitRepository> myAllRepositories;
|
||||
private final ChangesBrowser myChangesBrowser;
|
||||
private final CheckboxTree myTree;
|
||||
private final DefaultTreeModel myTreeModel;
|
||||
private final CheckedTreeNode myRootNode;
|
||||
private final ReentrantReadWriteLock TREE_CONSTRUCTION_LOCK = new ReentrantReadWriteLock();
|
||||
private boolean myTreeWasConstructed;
|
||||
private final MyTreeCellRenderer myTreeCellRenderer;
|
||||
|
||||
GitPushLog(@NotNull Project project, @NotNull Collection<GitRepository> repositories, @NotNull final Consumer<Boolean> checkboxListener) {
|
||||
myProject = project;
|
||||
myAllRepositories = repositories;
|
||||
|
||||
myRootNode = new CheckedTreeNode(null);
|
||||
myRootNode.add(new DefaultMutableTreeNode(new FakeCommit()));
|
||||
|
||||
myTreeModel = new DefaultTreeModel(myRootNode);
|
||||
myTreeCellRenderer = new MyTreeCellRenderer();
|
||||
myTree = new CheckboxTree(myTreeCellRenderer, myRootNode) {
|
||||
@Override
|
||||
protected void onNodeStateChanged(CheckedTreeNode node) {
|
||||
Object userObject = node.getUserObject();
|
||||
if (userObject instanceof GitRepository) {
|
||||
checkboxListener.consume(node.isChecked());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTipText(MouseEvent event) {
|
||||
final TreePath path = myTree.getPathForLocation(event.getX(), event.getY());
|
||||
if (path == null) {
|
||||
return "";
|
||||
}
|
||||
Object node = path.getLastPathComponent();
|
||||
if (node == null || (!(node instanceof DefaultMutableTreeNode))) {
|
||||
return "";
|
||||
}
|
||||
Object userObject = ((DefaultMutableTreeNode)node).getUserObject();
|
||||
if (userObject instanceof GitCommit) {
|
||||
GitCommit commit = (GitCommit)userObject;
|
||||
return getHashString(commit) + " " + getDateString(commit) + " by " + commit.getAuthor().getName() + "\n\n" +
|
||||
IssueLinkHtmlRenderer.formatTextWithLinks(myProject, commit.getFullMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
myTree.setRootVisible(false);
|
||||
TreeUtil.expandAll(myTree);
|
||||
|
||||
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
|
||||
myTree.addTreeSelectionListener(new TreeSelectionListener() {
|
||||
@Override
|
||||
public void valueChanged(TreeSelectionEvent e) {
|
||||
DefaultMutableTreeNode node = (DefaultMutableTreeNode) myTree.getLastSelectedPathComponent();
|
||||
if (node != null) {
|
||||
Object nodeInfo = node.getUserObject();
|
||||
if (nodeInfo instanceof GitCommit) {
|
||||
myChangesBrowser.getViewer().setEmptyText("No differences");
|
||||
myChangesBrowser.setChangesToDisplay(new ArrayList<Change>(((GitCommit)nodeInfo).getChanges()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setDefaultEmptyText();
|
||||
myChangesBrowser.setChangesToDisplay(Collections.<Change>emptyList());
|
||||
}
|
||||
});
|
||||
ToolTipManager.sharedInstance().registerComponent(myTree);
|
||||
|
||||
myChangesBrowser = new ChangesBrowser(project, null, Collections.<Change>emptyList(), null, false, true, null, ChangesBrowser.MyUseCase.LOCAL_CHANGES, null);
|
||||
myChangesBrowser.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myTree);
|
||||
setDefaultEmptyText();
|
||||
|
||||
Splitter splitter = new Splitter(false, 0.7f);
|
||||
splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myTree));
|
||||
splitter.setSecondComponent(myChangesBrowser);
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
add(splitter);
|
||||
}
|
||||
|
||||
private void setDefaultEmptyText() {
|
||||
myChangesBrowser.getViewer().setEmptyText("No commits selected");
|
||||
}
|
||||
|
||||
// Make changes available for diff action
|
||||
@Override
|
||||
public void calcData(DataKey key, DataSink sink) {
|
||||
if (VcsDataKeys.CHANGES.equals(key)) {
|
||||
DefaultMutableTreeNode[] selectedNodes = myTree.getSelectedNodes(DefaultMutableTreeNode.class, null);
|
||||
if (selectedNodes.length == 0) {
|
||||
return;
|
||||
}
|
||||
Object object = selectedNodes[0].getUserObject();
|
||||
if (object instanceof GitCommit) {
|
||||
sink.put(key, ArrayUtil.toObjectArray(((GitCommit)object).getChanges(), Change.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public JComponent getPreferredFocusComponent() {
|
||||
return myTree;
|
||||
}
|
||||
|
||||
void setCommits(@NotNull GitCommitsByRepoAndBranch commits) {
|
||||
try {
|
||||
TREE_CONSTRUCTION_LOCK.writeLock().lock();
|
||||
myRootNode.removeAllChildren();
|
||||
createNodes(commits);
|
||||
myTreeModel.nodeStructureChanged(myRootNode);
|
||||
myTree.setModel(myTreeModel); // TODO: why doesn't it repaint otherwise?
|
||||
TreeUtil.expandAll(myTree);
|
||||
selectFirstCommit();
|
||||
collapseEmptyRepoNodes(commits);
|
||||
myTreeWasConstructed = true;
|
||||
}
|
||||
finally {
|
||||
TREE_CONSTRUCTION_LOCK.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectFirstCommit() {
|
||||
DefaultMutableTreeNode firstLeaf = myRootNode.getFirstLeaf();
|
||||
if (firstLeaf == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Enumeration enumeration = myRootNode.depthFirstEnumeration();
|
||||
DefaultMutableTreeNode node = null;
|
||||
while (enumeration.hasMoreElements()) {
|
||||
node = (DefaultMutableTreeNode) enumeration.nextElement();
|
||||
if (node.isLeaf() && node.getUserObject() instanceof GitCommit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (node == null) {
|
||||
node = firstLeaf;
|
||||
}
|
||||
myTree.setSelectionPath(new TreePath(node.getPath()));
|
||||
}
|
||||
|
||||
private void collapseEmptyRepoNodes(GitCommitsByRepoAndBranch commits) {
|
||||
Enumeration enumeration = myRootNode.breadthFirstEnumeration();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
DefaultMutableTreeNode node = (DefaultMutableTreeNode) enumeration.nextElement();
|
||||
Object userObject = node.getUserObject();
|
||||
if (userObject == null) {
|
||||
// root object => seeking further
|
||||
}
|
||||
else if (userObject instanceof GitRepository) {
|
||||
if (commits.get((GitRepository)userObject).isEmpty()) {
|
||||
myTree.collapsePath(new TreePath(node.getPath()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// we're looking to the breadthFirstEnumeration => all repository nodes have already been enumerated
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createNodes(@NotNull GitCommitsByRepoAndBranch commits) {
|
||||
for (GitRepository repository : sortRepositories(commits)) {
|
||||
GitCommitsByBranch commitsByBranch = commits.get(repository);
|
||||
createRepoNode(repository, commitsByBranch, myRootNode);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<GitRepository> sortRepositories(@NotNull final GitCommitsByRepoAndBranch commits) {
|
||||
List<GitRepository> repos = new ArrayList<GitRepository>(commits.getRepositories());
|
||||
Collections.sort(repos, new Comparator<GitRepository>() {
|
||||
@Override public int compare(GitRepository r1, GitRepository r2) {
|
||||
// empty repositories - to the end
|
||||
if (commits.get(r1).isEmpty() && !commits.get(r2).isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
if (commits.get(r2).isEmpty() && !commits.get(r1).isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return r1.getPresentableUrl().compareTo(r2.getPresentableUrl());
|
||||
}
|
||||
});
|
||||
return repos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the node with subnodes for a repository and adds it to the rootNode.
|
||||
* If there is only one repo in the project, doesn't create a node for the repository, and adds subnodes directly to the rootNode.
|
||||
*/
|
||||
private void createRepoNode(@NotNull GitRepository repository, @NotNull GitCommitsByBranch commitsByBranch,
|
||||
@NotNull DefaultMutableTreeNode rootNode) {
|
||||
DefaultMutableTreeNode parentNode;
|
||||
if (GitUtil.justOneGitRepository(myProject)) {
|
||||
parentNode = rootNode;
|
||||
} else {
|
||||
parentNode = new CheckedTreeNode(repository);
|
||||
if (commitsByBranch.isEmpty()) {
|
||||
((CheckedTreeNode)parentNode).setChecked(false);
|
||||
}
|
||||
rootNode.add(parentNode);
|
||||
}
|
||||
|
||||
for (GitBranch branch : sortBranches(commitsByBranch.getBranches())) {
|
||||
DefaultMutableTreeNode branchNode = createBranchNode(branch, commitsByBranch.get(branch));
|
||||
parentNode.add(branchNode);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<GitBranch> sortBranches(@NotNull Collection<GitBranch> branches) {
|
||||
List<GitBranch> sortedBranches = new ArrayList<GitBranch>(branches);
|
||||
Collections.sort(sortedBranches, new Comparator<GitBranch>() {
|
||||
@Override public int compare(GitBranch o1, GitBranch o2) {
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
}
|
||||
});
|
||||
return sortedBranches;
|
||||
}
|
||||
|
||||
private static DefaultMutableTreeNode createBranchNode(@NotNull GitBranch branch, @NotNull GitPushBranchInfo branchInfo) {
|
||||
DefaultMutableTreeNode branchNode = new DefaultMutableTreeNode(branchInfo);
|
||||
for (GitCommit commit : branchInfo.getCommits()) {
|
||||
branchNode.add(new DefaultMutableTreeNode(commit));
|
||||
}
|
||||
if (branchInfo.isNewBranchCreated()) {
|
||||
branchNode.add(new DefaultMutableTreeNode(new MoreCommitsToShow()));
|
||||
}
|
||||
return branchNode;
|
||||
}
|
||||
|
||||
void displayError(String message) {
|
||||
DefaultMutableTreeNode titleNode = new DefaultMutableTreeNode("Error: couldn't collect commits to be pushed");
|
||||
DefaultMutableTreeNode detailNode = new DefaultMutableTreeNode(message);
|
||||
myRootNode.add(titleNode);
|
||||
myRootNode.add(detailNode);
|
||||
myTreeModel.reload(myRootNode);
|
||||
TreeUtil.expandAll(myTree);
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return repositories selected (via checkboxes) to be pushed.
|
||||
*/
|
||||
Collection<GitRepository> getSelectedRepositories() {
|
||||
if (myAllRepositories.size() == 1) {
|
||||
return myAllRepositories;
|
||||
}
|
||||
|
||||
try {
|
||||
TREE_CONSTRUCTION_LOCK.readLock().lock(); // wait for tree to be constructed
|
||||
if (!myTreeWasConstructed) {
|
||||
return myAllRepositories;
|
||||
}
|
||||
else {
|
||||
Collection<GitRepository> selectedRepositories = new ArrayList<GitRepository>(myAllRepositories.size());
|
||||
if (myRootNode.getChildCount() == 0) { // the method is requested before tree construction began => returning all repos.
|
||||
return myAllRepositories;
|
||||
}
|
||||
|
||||
for (int i = 0; i < myRootNode.getChildCount(); i++) {
|
||||
TreeNode child = myRootNode.getChildAt(i);
|
||||
if (child instanceof CheckedTreeNode) {
|
||||
CheckedTreeNode node = (CheckedTreeNode)child;
|
||||
if (node.isChecked()) {
|
||||
if (node.getUserObject() instanceof GitRepository) {
|
||||
selectedRepositories.add((GitRepository)node.getUserObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedRepositories;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
TREE_CONSTRUCTION_LOCK.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getDateString(@NotNull GitCommit commit) {
|
||||
return DvcsUtil.getDateString(commit);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getHashString(@NotNull GitCommit commit) {
|
||||
return DvcsUtil.getShortHash(commit.getId().toString());
|
||||
}
|
||||
|
||||
private static class MyTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
|
||||
|
||||
@Override
|
||||
public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
|
||||
Object userObject;
|
||||
if (value instanceof CheckedTreeNode) {
|
||||
userObject = ((CheckedTreeNode)value).getUserObject();
|
||||
} else if (value instanceof DefaultMutableTreeNode) {
|
||||
userObject = ((DefaultMutableTreeNode)value).getUserObject();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ColoredTreeCellRenderer renderer = getTextRenderer();
|
||||
if (userObject instanceof GitCommit) {
|
||||
GitCommit commit = (GitCommit)userObject;
|
||||
renderer.append(commit.getSubject(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, getTextRenderer().getForeground()));
|
||||
renderer.setToolTipText(getHashString(commit) + " " + getDateString(commit));
|
||||
}
|
||||
else if (userObject instanceof GitRepository) {
|
||||
String repositoryPath = DvcsUtil.getShortRepositoryName((GitRepository)userObject);
|
||||
renderer.append(repositoryPath, SimpleTextAttributes.GRAY_ATTRIBUTES);
|
||||
}
|
||||
else if (userObject instanceof GitPushBranchInfo) {
|
||||
GitPushBranchInfo branchInfo = (GitPushBranchInfo) userObject;
|
||||
GitBranch fromBranch = branchInfo.getSourceBranch();
|
||||
GitBranch dest = branchInfo.getDestBranch();
|
||||
|
||||
GitPushBranchInfo.Type type = branchInfo.getType();
|
||||
final String showingRecentCommits = ", showing " + GitPusher.RECENT_COMMITS_NUMBER + " recent commits";
|
||||
String text = fromBranch.getName();
|
||||
SimpleTextAttributes attrs = SimpleTextAttributes.REGULAR_ATTRIBUTES;
|
||||
String additionalText = "";
|
||||
switch (type) {
|
||||
case STANDARD:
|
||||
text += " -> " + dest.getName();
|
||||
if (branchInfo.getCommits().isEmpty()) {
|
||||
additionalText = " nothing to push";
|
||||
}
|
||||
break;
|
||||
case NEW_BRANCH:
|
||||
text += " -> +" + dest.getName();
|
||||
attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
|
||||
additionalText = " new branch will be created" + showingRecentCommits;
|
||||
break;
|
||||
case NO_TRACKED_OR_TARGET:
|
||||
attrs = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
|
||||
additionalText = " no tracked branch. Use checkbox below to push branch to manually specified" + showingRecentCommits;
|
||||
break;
|
||||
}
|
||||
renderer.append(text, attrs);
|
||||
renderer.append(additionalText, new SimpleTextAttributes(SimpleTextAttributes.STYLE_SMALLER, UIUtil.getInactiveTextColor()));
|
||||
}
|
||||
else if (userObject instanceof FakeCommit) {
|
||||
int spaces = 6 + 15 + 3 + 30;
|
||||
String s = String.format("%" + spaces + "s", " ");
|
||||
renderer.append(s, new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, renderer.getBackground()));
|
||||
}
|
||||
else if (userObject instanceof MoreCommitsToShow) {
|
||||
renderer.append("...");
|
||||
}
|
||||
else {
|
||||
renderer.append(userObject == null ? "" : userObject.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeCommit {
|
||||
}
|
||||
|
||||
private static class MoreCommitsToShow {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Push result as reported by {@code git push} command.
|
||||
*
|
||||
* @see GitPushNativeResultParser
|
||||
* @see GitPushRepoResult
|
||||
*/
|
||||
class GitPushNativeResult {
|
||||
|
||||
enum Type {
|
||||
SUCCESS,
|
||||
FORCED_UPDATE,
|
||||
NEW_REF,
|
||||
REJECTED,
|
||||
DELETED,
|
||||
UP_TO_DATE,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@NotNull private final Type myType;
|
||||
@Nullable private final String myRange;
|
||||
@Nullable private final String myErrorOutput;
|
||||
|
||||
private GitPushNativeResult(@NotNull Type type, @Nullable String range, @Nullable String errorOutput) {
|
||||
myType = type;
|
||||
myRange = range;
|
||||
myErrorOutput = errorOutput;
|
||||
}
|
||||
|
||||
public GitPushNativeResult(@NotNull Type type, @Nullable String range) {
|
||||
this(type, range, null);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitPushNativeResult error(@NotNull String errorOutput) {
|
||||
return new GitPushNativeResult(Type.ERROR, null, errorOutput);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Type getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getRange() {
|
||||
return myRange;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getErrorOutput() {
|
||||
return myErrorOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s: '%s', stderr:[%s]", myType, myRange, myErrorOutput);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Parses the output received from git push and returns a result.
|
||||
* NB: It is assumed that only one ref is pushed => there is only one result in the output.
|
||||
*
|
||||
* Output format described by git-push man:
|
||||
* <pre>
|
||||
* The status of the push is output in tabular form, with each line representing the status of a single ref.
|
||||
* If --porcelain is used, then each line of the output is of the form:
|
||||
*
|
||||
* <flag> \t <from>:<to> \t <summary> (<reason>)
|
||||
*
|
||||
* The status of up-to-date refs is shown only if --porcelain or --verbose option is used.
|
||||
*
|
||||
* flag
|
||||
* A single character indicating the status of the ref:
|
||||
* (space)
|
||||
* for a successfully pushed fast-forward;
|
||||
* +
|
||||
* for a successful forced update;
|
||||
* -
|
||||
* for a successfully deleted ref;
|
||||
* *
|
||||
* for a successfully pushed new ref;
|
||||
* !
|
||||
* for a ref that was rejected or failed to push; and
|
||||
* =
|
||||
* for a ref that was up to date and did not need pushing.
|
||||
*
|
||||
* summary
|
||||
* For a successfully pushed ref, the summary shows the old and new values of the ref in a form
|
||||
* suitable for using as an argument to git log (this is <old>..<new> in most cases, and
|
||||
* <old>...<new> for forced non-fast-forward updates).
|
||||
*
|
||||
* For a failed update, more details are given:
|
||||
* rejected
|
||||
* Git did not try to send the ref at all, typically because it is not a fast-forward and you
|
||||
* did not force the update.
|
||||
*
|
||||
* remote rejected
|
||||
* The remote end refused the update. Usually caused by a hook on the remote side, or because
|
||||
* the remote repository has one of the following safety options in effect:
|
||||
* receive.denyCurrentBranch (for pushes to the checked out branch), receive.denyNonFastForwards
|
||||
* (for forced non-fast-forward updates), receive.denyDeletes or receive.denyDeleteCurrent. See
|
||||
* git-config(1).
|
||||
*
|
||||
* remote failure
|
||||
* The remote end did not report the successful update of the ref, perhaps because of a
|
||||
* temporary error on the remote side, a break in the network connection, or other transient
|
||||
* error.
|
||||
*
|
||||
* from
|
||||
* The name of the local ref being pushed, minus its refs/<type>/ prefix. In the case of deletion,
|
||||
* the name of the local ref is omitted.
|
||||
*
|
||||
* to
|
||||
* The name of the remote ref being updated, minus its refs/<type>/ prefix.
|
||||
*
|
||||
* reason
|
||||
* A human-readable explanation. In the case of successfully pushed refs, no explanation is needed.
|
||||
* For a failed ref, the reason for failure is described.
|
||||
* </pre>
|
||||
*/
|
||||
public class GitPushNativeResultParser {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushNativeResultParser.class);
|
||||
private static final Pattern PATTERN = Pattern.compile("^.*([ +\\-\\*!=])\\s(\\S+):(\\S+)\\s(\\S+).*$");
|
||||
private static final Pattern RANGE = Pattern.compile("[0-9a-f]+[\\.]{2,3}[0-9a-f]+");
|
||||
|
||||
@Nullable
|
||||
public static GitPushNativeResult parse(@NotNull List<String> output) {
|
||||
for (String line : output) {
|
||||
Matcher matcher = PATTERN.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
return parseRefResult(matcher, line);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GitPushNativeResult parseRefResult(Matcher matcher, String line) {
|
||||
String flag = matcher.group(1);
|
||||
String from = matcher.group(2);
|
||||
String to = matcher.group(3);
|
||||
String summary = matcher.group(4);
|
||||
|
||||
GitPushNativeResult.Type type = parseType(flag);
|
||||
if (type == null) {
|
||||
LOG.error("Couldn't parse push result type from flag [" + flag + "] in [" + line + "]");
|
||||
return null;
|
||||
}
|
||||
String range = RANGE.matcher(summary).matches() ? summary : null;
|
||||
return new GitPushNativeResult(type, range);
|
||||
}
|
||||
|
||||
private static GitPushNativeResult.Type parseType(String flag) {
|
||||
switch(flag.charAt(0)) {
|
||||
case ' ' : return GitPushNativeResult.Type.SUCCESS;
|
||||
case '+' : return GitPushNativeResult.Type.FORCED_UPDATE;
|
||||
case '-' : return GitPushNativeResult.Type.DELETED;
|
||||
case '*' : return GitPushNativeResult.Type.NEW_REF;
|
||||
case '!' : return GitPushNativeResult.Type.REJECTED;
|
||||
case '=' : return GitPushNativeResult.Type.UP_TO_DATE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.dvcs.push.PushSpec;
|
||||
import com.intellij.history.Label;
|
||||
import com.intellij.history.LocalHistory;
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import git4idea.*;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.commands.Git;
|
||||
import git4idea.commands.GitCommandResult;
|
||||
import git4idea.commands.GitLineHandlerListener;
|
||||
import git4idea.commands.GitStandardProgressAnalyzer;
|
||||
import git4idea.config.GitVcsSettings;
|
||||
import git4idea.config.UpdateMethod;
|
||||
import git4idea.history.GitHistoryUtils;
|
||||
import git4idea.merge.MergeChangeCollector;
|
||||
import git4idea.repo.GitBranchTrackInfo;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.settings.GitPushSettings;
|
||||
import git4idea.update.GitUpdateProcess;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Executes git push operation:
|
||||
* <ul>
|
||||
* <li>Calls push for the given repositories with given parameters;</li>
|
||||
* <li>Collects results;</li>
|
||||
* <li>If push is rejected, proposes to update via merge or rebase;</li>
|
||||
* <li>Shows a notification about push result</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class GitPushOperation {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushOperation.class);
|
||||
private static final int MAX_PUSH_ATTEMPTS = 10;
|
||||
|
||||
private final Project myProject;
|
||||
private final Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> myPushSpecs;
|
||||
private final boolean myForce;
|
||||
private final Git myGit;
|
||||
private final ProgressIndicator myProgressIndicator;
|
||||
private final GitVcsSettings mySettings;
|
||||
private final GitPushSettings myPushSettings;
|
||||
private final GitPlatformFacade myPlatformFacade;
|
||||
|
||||
public GitPushOperation(Project project, Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> pushSpecs, boolean force) {
|
||||
myProject = project;
|
||||
myPushSpecs = pushSpecs;
|
||||
myForce = force;
|
||||
myGit = ServiceManager.getService(Git.class);
|
||||
myProgressIndicator = ObjectUtils.notNull(ProgressManager.getInstance().getProgressIndicator(), new EmptyProgressIndicator());
|
||||
mySettings = GitVcsSettings.getInstance(myProject);
|
||||
myPushSettings = GitPushSettings.getInstance(myProject);
|
||||
myPlatformFacade = ServiceManager.getService(project, GitPlatformFacade.class);
|
||||
|
||||
Map<GitRepository, GitRevisionNumber> currentHeads = ContainerUtil.newHashMap();
|
||||
for (GitRepository repository : pushSpecs.keySet()) {
|
||||
repository.update();
|
||||
String head = repository.getCurrentRevision();
|
||||
if (head == null) {
|
||||
LOG.error("This repository has no commits");
|
||||
}
|
||||
else {
|
||||
currentHeads.put(repository, new GitRevisionNumber(head));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitPushResult execute() {
|
||||
PushUpdateSettings updateSettings = readPushUpdateSettings();
|
||||
Label beforePushLabel = null;
|
||||
Label afterPushLabel = null;
|
||||
Map<GitRepository, String> preUpdatePositions = updateRootInfoAndRememberPositions();
|
||||
|
||||
final Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap();
|
||||
Map<GitRepository, GitUpdateResult> updatedRoots = ContainerUtil.newHashMap();
|
||||
|
||||
try {
|
||||
Collection<GitRepository> remainingRoots = myPushSpecs.keySet();
|
||||
for (int pushAttempt = 0;
|
||||
pushAttempt < MAX_PUSH_ATTEMPTS && !remainingRoots.isEmpty();
|
||||
pushAttempt++, remainingRoots = getRejectedAndNotPushed(results)) {
|
||||
Map<GitRepository, GitPushRepoResult> resultMap = push(remainingRoots);
|
||||
results.putAll(resultMap);
|
||||
|
||||
GroupedPushResult result = GroupedPushResult.group(resultMap);
|
||||
|
||||
// stop on first error
|
||||
if (!result.errors.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// propose to update if rejected
|
||||
if (!result.rejected.isEmpty()) {
|
||||
boolean shouldUpdate = true;
|
||||
if (pushingToNotTrackedBranch(result.rejected)) {
|
||||
shouldUpdate = false;
|
||||
}
|
||||
else if (pushAttempt == 0 && !mySettings.autoUpdateIfPushRejected()) {
|
||||
updateSettings = showDialogAndGetExitCode(result.rejected.keySet(), updateSettings);
|
||||
if (updateSettings != null) {
|
||||
savePushUpdateSettings(updateSettings);
|
||||
}
|
||||
else {
|
||||
shouldUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (beforePushLabel == null) { // put the label only before the very first update
|
||||
beforePushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
|
||||
}
|
||||
Collection<GitRepository> rootsToUpdate = updateSettings.shouldUpdateAllRoots() ?
|
||||
myPushSpecs.keySet() :
|
||||
result.rejected.keySet();
|
||||
GitUpdateResult updateResult = update(rootsToUpdate, updateSettings.getUpdateMethod());
|
||||
for (GitRepository repository : rootsToUpdate) {
|
||||
updatedRoots.put(repository, updateResult); // TODO update result in GitUpdateProcess is a single for several roots
|
||||
}
|
||||
if (!updateResult.isSuccess() ||
|
||||
updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS || updateResult == GitUpdateResult.INCOMPLETE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (beforePushLabel != null) {
|
||||
afterPushLabel = LocalHistory.getInstance().putSystemLabel(myProject, "After push");
|
||||
}
|
||||
}
|
||||
return prepareCombinedResult(results, updatedRoots, preUpdatePositions, beforePushLabel, afterPushLabel);
|
||||
}
|
||||
|
||||
private static boolean pushingToNotTrackedBranch(@NotNull Map<GitRepository, GitPushRepoResult> rejected) {
|
||||
return ContainerUtil.exists(rejected.entrySet(), new Condition<Map.Entry<GitRepository, GitPushRepoResult>>() {
|
||||
@Override
|
||||
public boolean value(Map.Entry<GitRepository, GitPushRepoResult> entry) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitLocalBranch currentBranch = repository.getCurrentBranch();
|
||||
assert currentBranch != null;
|
||||
GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
|
||||
return trackInfo == null || !trackInfo.getRemoteBranch().getFullName().equals(entry.getValue().getTargetBranch());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<GitRepository> getRejectedAndNotPushed(@NotNull final Map<GitRepository, GitPushRepoResult> results) {
|
||||
return ContainerUtil.filter(results.keySet(), new Condition<GitRepository>() {
|
||||
@Override
|
||||
public boolean value(GitRepository repository) {
|
||||
return results.get(repository).getType() == GitPushRepoResult.Type.REJECTED ||
|
||||
results.get(repository).getType() == GitPushRepoResult.Type.NOT_PUSHED;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<GitRepository, String> updateRootInfoAndRememberPositions() {
|
||||
return ContainerUtil.map2Map(myPushSpecs.keySet(),
|
||||
new Function<GitRepository, Pair<GitRepository, String>>() {
|
||||
@Override
|
||||
public Pair<GitRepository, String> fun(GitRepository repository) {
|
||||
repository.update();
|
||||
return Pair.create(repository, repository.getCurrentRevision());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private GitPushResult prepareCombinedResult(final Map<GitRepository, GitPushRepoResult> allRoots,
|
||||
final Map<GitRepository, GitUpdateResult> updatedRoots,
|
||||
final Map<GitRepository, String> preUpdatePositions,
|
||||
Label beforeUpdateLabel,
|
||||
Label afterUpdateLabel) {
|
||||
Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newHashMap();
|
||||
UpdatedFiles updatedFiles = UpdatedFiles.create();
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : allRoots.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult simpleResult = entry.getValue();
|
||||
GitUpdateResult updateResult = updatedRoots.get(repository);
|
||||
if (updateResult == null) {
|
||||
results.put(repository, simpleResult);
|
||||
}
|
||||
else {
|
||||
collectUpdatedFiles(updatedFiles, repository, preUpdatePositions.get(repository));
|
||||
results.put(repository, GitPushRepoResult.addUpdateResult(simpleResult, updateResult));
|
||||
}
|
||||
}
|
||||
return new GitPushResult(results, updatedFiles, beforeUpdateLabel, afterUpdateLabel);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<GitRepository, GitPushRepoResult> push(@NotNull Collection<GitRepository> repositories) {
|
||||
Map<GitRepository, GitPushRepoResult> results = ContainerUtil.newLinkedHashMap();
|
||||
for (GitRepository repository : repositories) {
|
||||
PushSpec<GitPushSource, GitPushTarget> spec = myPushSpecs.get(repository);
|
||||
GitPushNativeResult result = doPush(repository, spec);
|
||||
LOG.debug("Pushed to " + DvcsUtil.getShortRepositoryName(repository) + ": " + result);
|
||||
|
||||
int commits = collectNumberOfPushedCommits(repository.getRoot(), result);
|
||||
|
||||
GitPushTarget target = spec.getTarget();
|
||||
assert target != null;
|
||||
GitPushRepoResult repoResult = GitPushRepoResult.convertFromNative(result, commits, spec.getSource().getBranch(), target.getBranch());
|
||||
LOG.debug("Converted result: " + repoResult);
|
||||
results.put(repository, repoResult);
|
||||
}
|
||||
|
||||
// fill other not-processed repositories as not-pushed
|
||||
for (GitRepository repository : repositories) {
|
||||
if (!results.containsKey(repository)) {
|
||||
PushSpec<GitPushSource, GitPushTarget> spec = myPushSpecs.get(repository);
|
||||
results.put(repository, GitPushRepoResult.notPushed(spec.getSource().getBranch(), spec.getTarget().getBranch()));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private int collectNumberOfPushedCommits(@NotNull VirtualFile root, @NotNull GitPushNativeResult result) {
|
||||
if (result.getType() != GitPushNativeResult.Type.SUCCESS) {
|
||||
return -1;
|
||||
}
|
||||
String range = result.getRange();
|
||||
if (range == null) {
|
||||
LOG.error("Range of pushed commits not reported in " + result);
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return GitHistoryUtils.history(myProject, root, range).size();
|
||||
}
|
||||
catch (VcsException e) {
|
||||
LOG.error("Couldn't collect commits from range " + range);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectUpdatedFiles(@NotNull UpdatedFiles updatedFiles, @NotNull GitRepository repository,
|
||||
@NotNull String preUpdatePosition) {
|
||||
MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), new GitRevisionNumber(preUpdatePosition));
|
||||
ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
|
||||
collector.collect(updatedFiles, exceptions);
|
||||
for (VcsException exception : exceptions) {
|
||||
LOG.info(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitPushNativeResult doPush(@NotNull GitRepository repository, @NotNull PushSpec<GitPushSource, GitPushTarget> pushSpec) {
|
||||
GitPushTarget target = pushSpec.getTarget();
|
||||
assert target != null;
|
||||
GitLocalBranch sourceBranch = pushSpec.getSource().getBranch();
|
||||
GitRemoteBranch targetBranch = target.getBranch();
|
||||
|
||||
GitLineHandlerListener progressListener = GitStandardProgressAnalyzer.createListener(myProgressIndicator);
|
||||
boolean setUpstream = pushSpec.getTarget().isNewBranchCreated() && !branchTrackingInfoIsSet(repository, sourceBranch);
|
||||
GitCommandResult res = myGit.push(repository, sourceBranch, targetBranch, myForce, setUpstream, progressListener);
|
||||
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(res.getOutput());
|
||||
if (result == null) {
|
||||
result = GitPushNativeResult.error(res.getErrorOutputAsJoinedString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean branchTrackingInfoIsSet(@NotNull GitRepository repository, @NotNull final GitLocalBranch source) {
|
||||
return ContainerUtil.exists(repository.getBranchTrackInfos(), new Condition<GitBranchTrackInfo>() {
|
||||
@Override
|
||||
public boolean value(GitBranchTrackInfo info) {
|
||||
return info.getLocalBranch().equals(source);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void savePushUpdateSettings(@NotNull PushUpdateSettings PushUpdateSettings) {
|
||||
UpdateMethod updateMethod = PushUpdateSettings.getUpdateMethod();
|
||||
myPushSettings.setUpdateAllRoots(PushUpdateSettings.shouldUpdateAllRoots());
|
||||
myPushSettings.setUpdateMethod(updateMethod);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PushUpdateSettings readPushUpdateSettings() {
|
||||
boolean updateAllRoots = myPushSettings.shouldUpdateAllRoots();
|
||||
UpdateMethod updateMethod = myPushSettings.getUpdateMethod();
|
||||
return new PushUpdateSettings(updateAllRoots, updateMethod);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PushUpdateSettings showDialogAndGetExitCode(@NotNull final Set<GitRepository> repositories,
|
||||
@NotNull final PushUpdateSettings initialSettings) {
|
||||
final Ref<PushUpdateSettings> updateSettings = Ref.create();
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GitRejectedPushUpdateDialog dialog = new GitRejectedPushUpdateDialog(myProject, repositories, initialSettings);
|
||||
DialogManager.show(dialog);
|
||||
int exitCode = dialog.getExitCode();
|
||||
if (exitCode != DialogWrapper.CANCEL_EXIT_CODE) {
|
||||
mySettings.setAutoUpdateIfPushRejected(dialog.shouldAutoUpdateInFuture());
|
||||
updateSettings.set(new PushUpdateSettings(dialog.shouldUpdateAll(), convertUpdateMethodFromDialogExitCode(exitCode)));
|
||||
}
|
||||
}
|
||||
});
|
||||
return updateSettings.get();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static UpdateMethod convertUpdateMethodFromDialogExitCode(int exitCode) {
|
||||
switch (exitCode) {
|
||||
case GitRejectedPushUpdateDialog.MERGE_EXIT_CODE: return UpdateMethod.MERGE;
|
||||
case GitRejectedPushUpdateDialog.REBASE_EXIT_CODE: return UpdateMethod.REBASE;
|
||||
default: throw new IllegalStateException("Unexpected exit code: " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected GitUpdateResult update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
|
||||
GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ?
|
||||
GitUpdateProcess.UpdateMethod.MERGE :
|
||||
GitUpdateProcess.UpdateMethod.REBASE;
|
||||
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator,
|
||||
new HashSet<GitRepository>(rootsToUpdate), UpdatedFiles.create()).update(um);
|
||||
for (GitRepository repository : rootsToUpdate) {
|
||||
repository.getRoot().refresh(true, true);
|
||||
repository.update();
|
||||
}
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import com.intellij.openapi.util.Key;
|
||||
import git4idea.commands.GitLineHandlerListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
class GitPushRejectedDetector implements GitLineHandlerListener {
|
||||
|
||||
private static final Pattern REJECTED_PATTERN = Pattern.compile("\\s+! \\[rejected\\]\\s+(\\S+) -> (\\S+) .*");
|
||||
|
||||
private final Collection<RejectedRef> myRejectedRefs = new ArrayList<RejectedRef>();
|
||||
|
||||
@Override
|
||||
public void onLineAvailable(String line, Key outputType) {
|
||||
Matcher matcher = REJECTED_PATTERN.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
String src = matcher.group(1);
|
||||
String dst = matcher.group(2);
|
||||
myRejectedRefs.add(new RejectedRef(src, dst));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTerminated(int exitCode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startFailed(Throwable exception) {
|
||||
}
|
||||
|
||||
public boolean rejected() {
|
||||
return !myRejectedRefs.isEmpty();
|
||||
}
|
||||
|
||||
public Collection<String> getRejectedBranches() {
|
||||
Collection<String> branches = new ArrayList<String>(myRejectedRefs.size());
|
||||
for (RejectedRef rejectedRef : myRejectedRefs) {
|
||||
branches.add(rejectedRef.mySource);
|
||||
}
|
||||
return branches;
|
||||
}
|
||||
|
||||
static class RejectedRef {
|
||||
private final String mySource;
|
||||
private final String myDestination;
|
||||
|
||||
RejectedRef(String source, String destination) {
|
||||
myDestination = destination;
|
||||
mySource = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2011 JetBrains s.r.o.
|
||||
* Copyright 2000-2014 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.
|
||||
@@ -15,150 +15,134 @@
|
||||
*/
|
||||
package git4idea.push;
|
||||
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static git4idea.util.GitUIUtil.bold;
|
||||
import static git4idea.util.GitUIUtil.code;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* If an error happens, all push is unsuccessful, for all branches.
|
||||
* Otherwise we've got separate results for branches.
|
||||
* Result of pushing one repository.
|
||||
* <p/>
|
||||
* Includes information about the number of pushed commits (or -1 if undefined),
|
||||
* and tells whether the repository was updated after the push was rejected.
|
||||
*
|
||||
* @see git4idea.push.GitPushNativeResult
|
||||
*/
|
||||
final class GitPushRepoResult {
|
||||
class GitPushRepoResult {
|
||||
|
||||
enum Type {
|
||||
NOT_PUSHING,
|
||||
SUCCESS,
|
||||
SOME_REJECTED,
|
||||
NEW_BRANCH,
|
||||
FORCED,
|
||||
REJECTED,
|
||||
ERROR,
|
||||
CANCEL,
|
||||
NOT_AUTHORIZED
|
||||
NOT_PUSHED;
|
||||
}
|
||||
|
||||
private final Type myType;
|
||||
private final String myOutput;
|
||||
private final Map<GitBranch, GitPushBranchResult> myBranchResults;
|
||||
static Comparator<Type> TYPE_COMPARATOR = new Comparator<Type>() {
|
||||
@Override
|
||||
public int compare(Type o1, Type o2) {
|
||||
return o1.ordinal() - o2.ordinal();
|
||||
}
|
||||
};
|
||||
|
||||
GitPushRepoResult(@NotNull Type type, @NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
|
||||
@NotNull private final Type myType;
|
||||
private final int myCommits;
|
||||
@NotNull private final String mySourceBranch;
|
||||
@NotNull private final String myTargetBranch;
|
||||
@Nullable private final String myError;
|
||||
@Nullable private final GitUpdateResult myUpdateResult;
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult convertFromNative(@NotNull GitPushNativeResult result,
|
||||
int commits, @NotNull GitLocalBranch source, @NotNull GitRemoteBranch target) {
|
||||
return new GitPushRepoResult(convertType(result.getType()), commits, source.getFullName(), target.getFullName(),
|
||||
result.getErrorOutput(), null);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult notPushed(GitLocalBranch source, GitRemoteBranch target) {
|
||||
return new GitPushRepoResult(Type.NOT_PUSHED, -1, source.getFullName(), target.getFullName(), null, null);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult addUpdateResult(GitPushRepoResult original, GitUpdateResult updateResult) {
|
||||
return new GitPushRepoResult(original.getType(), original.getNumberOfPushedCommits(), original.getSourceBranch(),
|
||||
original.getTargetBranch(), original.getError(), updateResult);
|
||||
}
|
||||
|
||||
private GitPushRepoResult(@NotNull Type type, int pushedCommits, @NotNull String sourceBranch, @NotNull String targetBranch,
|
||||
@Nullable String error, @Nullable GitUpdateResult result) {
|
||||
myType = type;
|
||||
myBranchResults = resultsByBranch;
|
||||
myOutput = output;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult success(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
|
||||
return new GitPushRepoResult(Type.SUCCESS, resultsByBranch, output);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult error(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
|
||||
return new GitPushRepoResult(Type.ERROR, resultsByBranch, output);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult someRejected(@NotNull Map<GitBranch, GitPushBranchResult> resultsByBranch, @NotNull String output) {
|
||||
return new GitPushRepoResult(Type.SOME_REJECTED, resultsByBranch, output);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitPushRepoResult cancelled(@NotNull String output) {
|
||||
return new GitPushRepoResult(Type.CANCEL, Collections.<GitBranch, GitPushBranchResult>emptyMap(), output);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitPushRepoResult notAuthorized(@NotNull String output) {
|
||||
return new GitPushRepoResult(Type.NOT_AUTHORIZED, Collections.<GitBranch, GitPushBranchResult>emptyMap(), output);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushRepoResult notPushed() {
|
||||
return new GitPushRepoResult(Type.NOT_PUSHING, Collections.<GitBranch, GitPushBranchResult>emptyMap(), "");
|
||||
myCommits = pushedCommits;
|
||||
mySourceBranch = sourceBranch;
|
||||
myTargetBranch = targetBranch;
|
||||
myError = error;
|
||||
myUpdateResult = result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Type getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
boolean isOneOfErrors() {
|
||||
return myType == Type.ERROR || myType == Type.CANCEL || myType == Type.NOT_AUTHORIZED;
|
||||
|
||||
@Nullable
|
||||
GitUpdateResult getUpdateResult() {
|
||||
return myUpdateResult;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getOutput() {
|
||||
return myOutput;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<GitBranch, GitPushBranchResult> getBranchResults() {
|
||||
return myBranchResults;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitPushRepoResult remove(@NotNull GitBranch branch) {
|
||||
Map<GitBranch, GitPushBranchResult> resultsByBranch = new HashMap<GitBranch, GitPushBranchResult>();
|
||||
for (Map.Entry<GitBranch, GitPushBranchResult> entry : myBranchResults.entrySet()) {
|
||||
GitBranch b = entry.getKey();
|
||||
if (!b.equals(branch)) {
|
||||
resultsByBranch.put(b, entry.getValue());
|
||||
}
|
||||
}
|
||||
return new GitPushRepoResult(myType, resultsByBranch, myOutput);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return myBranchResults.isEmpty();
|
||||
int getNumberOfPushedCommits() {
|
||||
return myCommits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given results to this result.
|
||||
* In the case of conflict (i.e. different results for a branch), current result is preferred over the previous one.
|
||||
* Returns the branch we were pushing from, in the full-name format, e.g. {@code refs/heads/master}.
|
||||
*/
|
||||
void mergeFrom(@NotNull GitPushRepoResult repoResult) {
|
||||
for (Map.Entry<GitBranch, GitPushBranchResult> entry : repoResult.myBranchResults.entrySet()) {
|
||||
GitBranch branch = entry.getKey();
|
||||
GitPushBranchResult branchResult = entry.getValue();
|
||||
if (!myBranchResults.containsKey(branch)) { // otherwise current result is preferred
|
||||
myBranchResults.put(branch, branchResult);
|
||||
}
|
||||
}
|
||||
@NotNull
|
||||
String getSourceBranch() {
|
||||
return mySourceBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the branch we were pushing to, in the full-name format, e.g. {@code refs/remotes/origin/master}.
|
||||
*/
|
||||
@NotNull
|
||||
String getTargetBranch() {
|
||||
return myTargetBranch;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getError() {
|
||||
return myError;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getPerBranchesNonErrorReport() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int i = 0;
|
||||
for (Map.Entry<GitBranch, GitPushBranchResult> entry : myBranchResults.entrySet()) {
|
||||
GitBranch branch = entry.getKey();
|
||||
GitPushBranchResult branchResult = entry.getValue();
|
||||
|
||||
if (branchResult.isSuccess()) {
|
||||
sb.append(bold(branch.getName()) + ": pushed " + commits(branchResult.getNumberOfPushedCommits()));
|
||||
}
|
||||
else if (branchResult.isNewBranch()) {
|
||||
sb.append(bold(branch.getName()) + " pushed to new branch " + bold(branchResult.getTargetBranchName()));
|
||||
}
|
||||
else {
|
||||
sb.append(code(branch.getName())).append(": rejected");
|
||||
}
|
||||
|
||||
if (i < myBranchResults.size() - 1) {
|
||||
sb.append("<br/>");
|
||||
}
|
||||
private static Type convertType(@NotNull GitPushNativeResult.Type nativeType) {
|
||||
switch (nativeType) {
|
||||
case SUCCESS:
|
||||
return Type.SUCCESS;
|
||||
case FORCED_UPDATE:
|
||||
return Type.FORCED;
|
||||
case NEW_REF:
|
||||
return Type.NEW_BRANCH;
|
||||
case REJECTED:
|
||||
return Type.REJECTED;
|
||||
case UP_TO_DATE:
|
||||
return Type.SUCCESS;
|
||||
case ERROR:
|
||||
return Type.ERROR;
|
||||
case DELETED:
|
||||
default:
|
||||
throw new IllegalArgumentException("Conversion is not supported: " + nativeType);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String commits(int commitNum) {
|
||||
return commitNum + " " + StringUtil.pluralize("commit", commitNum);
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s (%d, '%s'), update: %s}", myType, myCommits, mySourceBranch, myUpdateResult);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2011 JetBrains s.r.o.
|
||||
* Copyright 2000-2014 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.
|
||||
@@ -16,367 +16,50 @@
|
||||
package git4idea.push;
|
||||
|
||||
import com.intellij.history.Label;
|
||||
import com.intellij.history.LocalHistory;
|
||||
import com.intellij.ide.BrowserUtil;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationListener;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.VcsNotifier;
|
||||
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
|
||||
import com.intellij.openapi.vcs.update.ActionInfo;
|
||||
import com.intellij.openapi.vcs.update.UpdateInfoTree;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitRevisionNumber;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.merge.MergeChangeCollector;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Combined push result for all affected repositories in the project.
|
||||
*/
|
||||
class GitPushResult {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushResult.class);
|
||||
@NotNull private final Map<GitRepository, GitPushRepoResult> myResults;
|
||||
@NotNull private final UpdatedFiles myUpdatedFiles;
|
||||
@Nullable private final Label myBeforeUpdateLabel;
|
||||
@Nullable private final Label myAfterUpdateLabel;
|
||||
|
||||
private final Project myProject;
|
||||
private final Map<GitRepository, GitPushRepoResult> myResults = new HashMap<GitRepository, GitPushRepoResult>();
|
||||
private final Map<GitRepository, GitRevisionNumber> myUpdateStarts = new HashMap<GitRepository, GitRevisionNumber>();
|
||||
private Label myBeforeUpdateLabel;
|
||||
|
||||
GitPushResult(@NotNull Project project) {
|
||||
this(project, null, new HashMap<GitRepository, GitRevisionNumber>());
|
||||
}
|
||||
|
||||
private GitPushResult(@NotNull Project project, @Nullable Label beforeUpdateLabel, @NotNull Map<GitRepository, GitRevisionNumber> updateStarts) {
|
||||
myProject = project;
|
||||
myBeforeUpdateLabel = beforeUpdateLabel;
|
||||
for (Map.Entry<GitRepository, GitRevisionNumber> entry : updateStarts.entrySet()) {
|
||||
myUpdateStarts.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
void append(@NotNull GitRepository repository, @NotNull GitPushRepoResult result) {
|
||||
myResults.put(repository, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* For the specified repositories remembers the revision when update process (actually, auto-update inside the push process) has started.
|
||||
*/
|
||||
void markUpdateStartIfNotMarked(@NotNull Collection<GitRepository> repositories) {
|
||||
if (myUpdateStarts.isEmpty()) {
|
||||
myBeforeUpdateLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
|
||||
}
|
||||
for (GitRepository repository : repositories) {
|
||||
if (!myUpdateStarts.containsKey(repository)) {
|
||||
String currentRevision = repository.getCurrentRevision();
|
||||
if (currentRevision != null) {
|
||||
myUpdateStarts.put(repository, new GitRevisionNumber(currentRevision));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean wasErrorCancelOrNotAuthorized() {
|
||||
for (GitPushRepoResult repoResult : myResults.values()) {
|
||||
if (repoResult.isOneOfErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
GitPushResult(@NotNull Map<GitRepository, GitPushRepoResult> results,
|
||||
@NotNull UpdatedFiles files,
|
||||
@Nullable Label beforeUpdateLabel,
|
||||
@Nullable Label afterUpdateLabel) {
|
||||
myResults = results;
|
||||
myUpdatedFiles = files;
|
||||
myAfterUpdateLabel = beforeUpdateLabel;
|
||||
myBeforeUpdateLabel = afterUpdateLabel;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GroupedResult group() {
|
||||
final Map<GitRepository, GitPushRepoResult> successfulResults = new HashMap<GitRepository, GitPushRepoResult>();
|
||||
final Map<GitRepository, GitPushRepoResult> errorResults = new HashMap<GitRepository, GitPushRepoResult>();
|
||||
final Map<GitRepository, GitPushRepoResult> rejectedResults = new HashMap<GitRepository, GitPushRepoResult>();
|
||||
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult repoResult = entry.getValue();
|
||||
switch (repoResult.getType()) {
|
||||
case SUCCESS:
|
||||
successfulResults.put(repository, repoResult);
|
||||
break;
|
||||
case ERROR:
|
||||
case CANCEL:
|
||||
case NOT_AUTHORIZED:
|
||||
errorResults.put(repository, repoResult);
|
||||
break;
|
||||
case NOT_PUSHING:
|
||||
break;
|
||||
case SOME_REJECTED:
|
||||
rejectedResults.put(repository, repoResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new GroupedResult(successfulResults, errorResults, rejectedResults);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return myResults.isEmpty();
|
||||
public Map<GitRepository, GitPushRepoResult> getResults() {
|
||||
return myResults;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitPushResult remove(@NotNull Map<GitRepository, GitBranch> repoBranchPairs) {
|
||||
GitPushResult result = new GitPushResult(myProject, myBeforeUpdateLabel, myUpdateStarts);
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult repoResult = entry.getValue();
|
||||
if (repoBranchPairs.containsKey(repository)) {
|
||||
GitPushRepoResult adjustedResult = repoResult.remove(repoBranchPairs.get(repository));
|
||||
if (!repoResult.isEmpty()) {
|
||||
result.append(repository, adjustedResult);
|
||||
}
|
||||
} else {
|
||||
result.append(repository, repoResult);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
public UpdatedFiles getUpdatedFiles() {
|
||||
return myUpdatedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given results to this result.
|
||||
* In the case of conflict (i.e. different results for a repository-branch pair), current result is preferred over the previous one.
|
||||
*/
|
||||
void mergeFrom(@Nullable GitPushResult previousResult) {
|
||||
if (previousResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : previousResult.myResults.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult repoResult = entry.getValue();
|
||||
if (myResults.containsKey(repository)) {
|
||||
myResults.get(repository).mergeFrom(previousResult.myResults.get(repository));
|
||||
} else {
|
||||
append(repository, repoResult);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<GitRepository, GitRevisionNumber> entry : previousResult.myUpdateStarts.entrySet()) {
|
||||
myUpdateStarts.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
myBeforeUpdateLabel = previousResult.myBeforeUpdateLabel;
|
||||
@Nullable
|
||||
public Label getBeforeUpdateLabel() {
|
||||
return myBeforeUpdateLabel;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<GitRepository, GitBranch> getRejectedPushesFromCurrentBranchToTrackedBranch(GitPushInfo pushInfo) {
|
||||
final Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch = new HashMap<GitRepository, GitBranch>();
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : group().myRejectedResults.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitBranch currentBranch = repository.getCurrentBranch();
|
||||
if (currentBranch == null) {
|
||||
continue;
|
||||
}
|
||||
GitPushRepoResult repoResult = entry.getValue();
|
||||
GitPushBranchResult curBranchResult = repoResult.getBranchResults().get(currentBranch);
|
||||
|
||||
if (curBranchResult == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String trackedBranchName;
|
||||
try {
|
||||
String simpleName = GitBranchUtil.getTrackedBranchName(myProject, repository.getRoot(), currentBranch.getName());
|
||||
if (simpleName == null) {
|
||||
continue;
|
||||
}
|
||||
if (simpleName.startsWith(GitBranch.REFS_HEADS_PREFIX)) {
|
||||
simpleName = simpleName.substring(GitBranch.REFS_HEADS_PREFIX.length());
|
||||
}
|
||||
String remote = GitBranchUtil.getTrackedRemoteName(myProject, repository.getRoot(), currentBranch.getName());
|
||||
if (remote == null) {
|
||||
continue;
|
||||
}
|
||||
trackedBranchName = remote + "/" + simpleName;
|
||||
}
|
||||
catch (VcsException e) {
|
||||
LOG.info("Couldn't get tracked branch for branch " + currentBranch, e);
|
||||
continue;
|
||||
}
|
||||
if (!pushInfo.getPushSpecs().get(repository).getDest().getName().equals(trackedBranchName)) {
|
||||
// push from current branch was rejected, but it was a push not to the tracked branch => ignore
|
||||
continue;
|
||||
}
|
||||
if (curBranchResult.isRejected()) {
|
||||
rejectedPushesForCurrentBranch.put(repository, currentBranch);
|
||||
}
|
||||
}
|
||||
return rejectedPushesForCurrentBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the HTML-formatted message from error outputs of failed repositories and notify.
|
||||
* If there is only 1 repository in the project, just returns the error without writing the repository url (to avoid confusion for people
|
||||
* with only 1 root ever).
|
||||
* Otherwise adds repository URL to the error that repository produced.
|
||||
*
|
||||
* The procedure also includes collecting for updated files (if an auto-update was performed during the push), which may be lengthy.
|
||||
*/
|
||||
void createPushNotificationAndNotify() {
|
||||
final UpdatedFiles updatedFiles = collectUpdatedFiles();
|
||||
|
||||
GroupedResult groupedResult = group();
|
||||
|
||||
boolean error = !groupedResult.myErrorResults.isEmpty();
|
||||
boolean rejected = !groupedResult.myRejectedResults.isEmpty();
|
||||
boolean success = !groupedResult.mySuccessfulResults.isEmpty();
|
||||
|
||||
boolean onlyError = error && !rejected && !success;
|
||||
boolean onlyRejected = rejected && !error && !success;
|
||||
final boolean onlySuccess = success && !rejected && !error;
|
||||
|
||||
int pushedCommitsNumber = calcPushedCommitTotalNumber(myResults);
|
||||
|
||||
String title;
|
||||
if (error) {
|
||||
if (onlyError) {
|
||||
title = "Push failed";
|
||||
} else {
|
||||
title = "Push partially failed";
|
||||
if (success) {
|
||||
title += ", " + commits(pushedCommitsNumber) + " pushed";
|
||||
}
|
||||
}
|
||||
} else if (rejected) {
|
||||
if (onlyRejected) {
|
||||
title = "Push rejected";
|
||||
} else {
|
||||
title = "Push partially rejected, " + commits(pushedCommitsNumber) + " pushed";
|
||||
}
|
||||
} else {
|
||||
title = "Push successful";
|
||||
}
|
||||
|
||||
String errorReport = reportForGroup(groupedResult.myErrorResults, GroupedResult.Type.ERROR);
|
||||
String successReport = reportForGroup(groupedResult.mySuccessfulResults, GroupedResult.Type.SUCCESS);
|
||||
String rejectedReport = reportForGroup(groupedResult.myRejectedResults, GroupedResult.Type.REJECT);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(errorReport);
|
||||
sb.append(rejectedReport);
|
||||
sb.append(successReport);
|
||||
|
||||
if (!updatedFiles.isEmpty()) {
|
||||
sb.append("<a href='UpdatedFiles'>View files updated during the push</a>");
|
||||
}
|
||||
|
||||
NotificationListener viewUpdateFilesListener = new ViewUpdatedFilesNotificationListener(updatedFiles);
|
||||
VcsNotifier vcsNotifier = VcsNotifier.getInstance(myProject);
|
||||
|
||||
if (onlySuccess) {
|
||||
vcsNotifier.notifySuccess(title, sb.toString(), viewUpdateFilesListener);
|
||||
}
|
||||
else if (error) {
|
||||
vcsNotifier.notifyError(title, sb.toString(), viewUpdateFilesListener);
|
||||
}
|
||||
else {
|
||||
vcsNotifier.notifyImportantWarning(title, sb.toString(), viewUpdateFilesListener);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private UpdatedFiles collectUpdatedFiles() {
|
||||
UpdatedFiles updatedFiles = UpdatedFiles.create();
|
||||
for (Map.Entry<GitRepository, GitRevisionNumber> updatedRepository : myUpdateStarts.entrySet()) {
|
||||
GitRepository repository = updatedRepository.getKey();
|
||||
final MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), updatedRepository.getValue());
|
||||
final ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
|
||||
collector.collect(updatedFiles, exceptions);
|
||||
for (VcsException exception : exceptions) {
|
||||
LOG.info(exception);
|
||||
}
|
||||
}
|
||||
return updatedFiles;
|
||||
}
|
||||
|
||||
private static int calcPushedCommitTotalNumber(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults) {
|
||||
int sum = 0;
|
||||
for (GitPushRepoResult pushRepoResult : successfulResults.values()) {
|
||||
for (GitPushBranchResult branchResult : pushRepoResult.getBranchResults().values()) {
|
||||
sum += branchResult.getNumberOfPushedCommits();
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String reportForGroup(@NotNull Map<GitRepository, GitPushRepoResult> groupResult, @NotNull GroupedResult.Type resultType) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : groupResult.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult result = entry.getValue();
|
||||
|
||||
if (!GitUtil.justOneGitRepository(myProject)) {
|
||||
sb.append("<code>" + repository.getPresentableUrl() + "</code>:<br/>");
|
||||
}
|
||||
if (resultType == GroupedResult.Type.SUCCESS || resultType == GroupedResult.Type.REJECT) {
|
||||
sb.append(result.getPerBranchesNonErrorReport());
|
||||
} else {
|
||||
sb.append(result.getOutput());
|
||||
}
|
||||
sb.append("<br/>");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
private static class GroupedResult {
|
||||
enum Type {
|
||||
SUCCESS,
|
||||
REJECT,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private final Map<GitRepository, GitPushRepoResult> mySuccessfulResults;
|
||||
private final Map<GitRepository, GitPushRepoResult> myErrorResults;
|
||||
private final Map<GitRepository, GitPushRepoResult> myRejectedResults;
|
||||
|
||||
GroupedResult(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults,
|
||||
@NotNull Map<GitRepository, GitPushRepoResult> errorResults,
|
||||
@NotNull Map<GitRepository, GitPushRepoResult> rejectedResults) {
|
||||
mySuccessfulResults = successfulResults;
|
||||
myErrorResults = errorResults;
|
||||
myRejectedResults = rejectedResults;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String commits(int commitNum) {
|
||||
return commitNum + " " + StringUtil.pluralize("commit", commitNum);
|
||||
}
|
||||
|
||||
private class ViewUpdatedFilesNotificationListener implements NotificationListener {
|
||||
private final UpdatedFiles myUpdatedFiles;
|
||||
|
||||
public ViewUpdatedFilesNotificationListener(UpdatedFiles updatedFiles) {
|
||||
myUpdatedFiles = updatedFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
|
||||
if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
|
||||
if (event.getDescription().equals("UpdatedFiles")) {
|
||||
ProjectLevelVcsManagerEx vcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject);
|
||||
UpdateInfoTree tree = vcsManager.showUpdateProjectInfo(myUpdatedFiles, "Update", ActionInfo.UPDATE, false);
|
||||
tree.setBefore(myBeforeUpdateLabel);
|
||||
tree.setAfter(LocalHistory.getInstance().putSystemLabel(myProject, "After push"));
|
||||
}
|
||||
else {
|
||||
BrowserUtil.browse(event.getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
@Nullable
|
||||
public Label getAfterUpdateLabel() {
|
||||
return myAfterUpdateLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.DvcsUtil;
|
||||
import com.intellij.dvcs.repo.RepositoryUtil;
|
||||
import com.intellij.history.Label;
|
||||
import com.intellij.ide.BrowserUtil;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationGroup;
|
||||
import com.intellij.notification.NotificationListener;
|
||||
import com.intellij.notification.NotificationType;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.VcsNotifier;
|
||||
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
|
||||
import com.intellij.openapi.vcs.update.ActionInfo;
|
||||
import com.intellij.openapi.vcs.update.UpdateInfoTree;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class GitPushResultNotification extends Notification {
|
||||
|
||||
public static final String VIEW_FILES_UPDATED_DURING_THE_PUSH = "<a href='UpdatedFiles'>View files updated during the push</a>";
|
||||
|
||||
public static final String UPDATE_WITH_RESOLVED_CONFLICTS = "push has been cancelled, because there were conflicts during update.<br/>" +
|
||||
"Check that conflicts were resolved correctly, and invoke push again.";
|
||||
public static final String INCOMPLETE_UPDATE = "push has been cancelled, because not all conflicts were resolved during update.<br/>" +
|
||||
"Resolve the conflicts and invoke push again.";
|
||||
public static final String UPDATE_WITH_ERRORS = "push was rejected, and update failed with error";
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushResultNotification.class);
|
||||
|
||||
public GitPushResultNotification(@NotNull String groupDisplayId,
|
||||
@NotNull String title,
|
||||
@NotNull String content,
|
||||
@NotNull NotificationType type,
|
||||
@Nullable NotificationListener listener) {
|
||||
super(groupDisplayId, title, content, type, listener);
|
||||
}
|
||||
|
||||
static GitPushResultNotification create(Project project, GitPushResult pushResult, boolean multiRepoProject) {
|
||||
GroupedPushResult grouped = GroupedPushResult.group(pushResult.getResults());
|
||||
|
||||
String title;
|
||||
NotificationType type;
|
||||
if (!grouped.errors.isEmpty()) {
|
||||
if (!grouped.successful.isEmpty()) {
|
||||
title = "Push partially failed";
|
||||
}
|
||||
else {
|
||||
title = "Push failed";
|
||||
}
|
||||
type = NotificationType.ERROR;
|
||||
}
|
||||
else if (!grouped.rejected.isEmpty()) {
|
||||
if (!grouped.successful.isEmpty()) {
|
||||
title = "Push partially rejected";
|
||||
}
|
||||
else {
|
||||
title = "Push rejected";
|
||||
}
|
||||
type = NotificationType.WARNING;
|
||||
}
|
||||
else {
|
||||
title = "Push successful";
|
||||
type = NotificationType.INFORMATION;
|
||||
}
|
||||
|
||||
String description = formDescription(pushResult.getResults(), multiRepoProject);
|
||||
|
||||
ViewUpdatedFilesNotificationListener listener = null;
|
||||
UpdatedFiles updatedFiles = pushResult.getUpdatedFiles();
|
||||
if (!updatedFiles.isEmpty()) {
|
||||
description += "<br/>" + VIEW_FILES_UPDATED_DURING_THE_PUSH;
|
||||
listener = new ViewUpdatedFilesNotificationListener(project, updatedFiles,
|
||||
pushResult.getBeforeUpdateLabel(), pushResult.getAfterUpdateLabel());
|
||||
}
|
||||
|
||||
NotificationGroup group = type == NotificationType.INFORMATION ?
|
||||
VcsNotifier.NOTIFICATION_GROUP_ID :
|
||||
VcsNotifier.IMPORTANT_ERROR_NOTIFICATION;
|
||||
|
||||
return new GitPushResultNotification(group.getDisplayId(), title, description, type, listener);
|
||||
}
|
||||
|
||||
private static String formDescription(Map<GitRepository, GitPushRepoResult> results, final boolean multiRepoProject) {
|
||||
List<Map.Entry<GitRepository, GitPushRepoResult>> entries = ContainerUtil.sorted(results.entrySet(),
|
||||
new Comparator<Map.Entry<GitRepository, GitPushRepoResult>>() {
|
||||
@Override
|
||||
public int compare(Map.Entry<GitRepository, GitPushRepoResult> o1, Map.Entry<GitRepository, GitPushRepoResult> o2) {
|
||||
// successful first
|
||||
int compareResultTypes = GitPushRepoResult.TYPE_COMPARATOR.compare(o1.getValue().getType(), o2.getValue().getType());
|
||||
if (compareResultTypes != 0) {
|
||||
return compareResultTypes;
|
||||
}
|
||||
return RepositoryUtil.REPOSITORY_COMPARATOR.compare(o1.getKey(), o2.getKey());
|
||||
}
|
||||
});
|
||||
|
||||
return StringUtil.join(entries, new Function<Map.Entry<GitRepository, GitPushRepoResult>, String>() {
|
||||
@Override
|
||||
public String fun(Map.Entry<GitRepository, GitPushRepoResult> entry) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult result = entry.getValue();
|
||||
|
||||
String description = formRepoDescription(result);
|
||||
if (!multiRepoProject) {
|
||||
description = StringUtil.capitalize(description);
|
||||
}
|
||||
else {
|
||||
description = DvcsUtil.getShortRepositoryName(repository) + ": " + description;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
}, "<br/>");
|
||||
}
|
||||
|
||||
private static String formRepoDescription(GitPushRepoResult result) {
|
||||
String description;
|
||||
String sourceBranch = GitBranchUtil.stripRefsPrefix(result.getSourceBranch());
|
||||
String targetBranch = GitBranchUtil.stripRefsPrefix(result.getTargetBranch());
|
||||
switch (result.getType()) {
|
||||
case SUCCESS:
|
||||
int commits = result.getNumberOfPushedCommits();
|
||||
description = String.format("pushed %d %s to %s", commits, StringUtil.pluralize("commit", commits), targetBranch);
|
||||
break;
|
||||
case NEW_BRANCH:
|
||||
description = String.format("pushed %s to new branch %s", sourceBranch, targetBranch);
|
||||
break;
|
||||
case FORCED:
|
||||
description = String.format("force pushed %s to %s", sourceBranch, targetBranch);
|
||||
break;
|
||||
case REJECTED:
|
||||
description = formDescriptionBasedOnUpdateResult(result.getUpdateResult(), targetBranch);
|
||||
break;
|
||||
case ERROR:
|
||||
description = String.format("failed with error: " + result.getError());
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unexpected push result: " + result);
|
||||
description = "";
|
||||
break;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
private static String formDescriptionBasedOnUpdateResult(GitUpdateResult updateResult, String targetBranch) {
|
||||
if (updateResult == null || updateResult == GitUpdateResult.SUCCESS || updateResult == GitUpdateResult.NOTHING_TO_UPDATE) {
|
||||
return String.format("push to %s was rejected", targetBranch);
|
||||
}
|
||||
else if (updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS) {
|
||||
return UPDATE_WITH_RESOLVED_CONFLICTS;
|
||||
}
|
||||
else if (updateResult == GitUpdateResult.INCOMPLETE) {
|
||||
return INCOMPLETE_UPDATE;
|
||||
}
|
||||
else {
|
||||
return UPDATE_WITH_ERRORS;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewUpdatedFilesNotificationListener implements NotificationListener {
|
||||
|
||||
private final Project myProject;
|
||||
private final UpdatedFiles myUpdatedFiles;
|
||||
private final Label myBeforeUpdateLabel;
|
||||
private final Label myAfterUpdateLabel;
|
||||
|
||||
public ViewUpdatedFilesNotificationListener(@NotNull Project project, @NotNull UpdatedFiles updatedFiles, Label label, Label label1) {
|
||||
myProject = project;
|
||||
myUpdatedFiles = updatedFiles;
|
||||
myBeforeUpdateLabel = label;
|
||||
myAfterUpdateLabel = label1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
|
||||
if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
|
||||
if (event.getDescription().equals("UpdatedFiles")) {
|
||||
ProjectLevelVcsManagerEx vcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject);
|
||||
UpdateInfoTree tree = vcsManager.showUpdateProjectInfo(myUpdatedFiles, "Update", ActionInfo.UPDATE, false);
|
||||
tree.setBefore(myBeforeUpdateLabel);
|
||||
tree.setAfter(myAfterUpdateLabel);
|
||||
}
|
||||
else {
|
||||
BrowserUtil.browse(event.getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+13
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2012 JetBrains s.r.o.
|
||||
* Copyright 2000-2014 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.
|
||||
@@ -15,41 +15,26 @@
|
||||
*/
|
||||
package git4idea.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushSource;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.repo.GitRemote;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitPushSpec {
|
||||
class GitPushSource implements PushSource {
|
||||
|
||||
@NotNull private final GitLocalBranch mySource;
|
||||
@NotNull private final GitRemoteBranch myDest;
|
||||
@NotNull private final GitLocalBranch myBranch;
|
||||
|
||||
GitPushSpec(@NotNull GitLocalBranch source, @NotNull GitRemoteBranch dest) {
|
||||
myDest = dest;
|
||||
mySource = source;
|
||||
GitPushSource(@NotNull GitLocalBranch branch) {
|
||||
myBranch = branch;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitRemote getRemote() {
|
||||
return myDest.getRemote();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitLocalBranch getSource() {
|
||||
return mySource;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitRemoteBranch getDest() {
|
||||
return myDest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mySource + "->" + myDest;
|
||||
public String getPresentation() {
|
||||
return myBranch.getName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GitLocalBranch getBranch() {
|
||||
return myBranch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.OutgoingCommitsProvider;
|
||||
import com.intellij.dvcs.push.PushSupport;
|
||||
import com.intellij.dvcs.push.PushTargetPanel;
|
||||
import com.intellij.dvcs.push.Pusher;
|
||||
import com.intellij.dvcs.repo.RepositoryManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vcs.AbstractVcs;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import git4idea.*;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.repo.GitBranchTrackInfo;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class GitPushSupport extends PushSupport<GitRepository, GitPushSource, GitPushTarget> {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushSupport.class);
|
||||
|
||||
@NotNull private final GitRepositoryManager myRepositoryManager;
|
||||
@NotNull private final GitVcs myVcs;
|
||||
@NotNull private final Pusher<GitRepository, GitPushSource, GitPushTarget> myPusher;
|
||||
@NotNull private final OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> myOutgoingCommitsProvider;
|
||||
|
||||
// instantiated from plugin.xml
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private GitPushSupport(@NotNull Project project, @NotNull GitRepositoryManager repositoryManager) {
|
||||
myRepositoryManager = repositoryManager;
|
||||
myVcs = ObjectUtils.assertNotNull(GitVcs.getInstance(project));
|
||||
myPusher = new GitPusher(project);
|
||||
myOutgoingCommitsProvider = new GitOutgoingCommitsProvider(project);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public AbstractVcs getVcs() {
|
||||
return myVcs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Pusher<GitRepository, GitPushSource, GitPushTarget> getPusher() {
|
||||
return myPusher;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public OutgoingCommitsProvider<GitRepository, GitPushSource, GitPushTarget> getOutgoingCommitsProvider() {
|
||||
return myOutgoingCommitsProvider;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public GitPushTarget getDefaultTarget(@NotNull GitRepository repository) {
|
||||
GitLocalBranch currentBranch = repository.getCurrentBranch();
|
||||
if (currentBranch == null) {
|
||||
return null;
|
||||
}
|
||||
GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
|
||||
if (trackInfo != null) {
|
||||
return new GitPushTarget(trackInfo.getRemoteBranch(), false);
|
||||
}
|
||||
return proposeTargetForNewBranch(repository, currentBranch);
|
||||
}
|
||||
|
||||
private static GitPushTarget proposeTargetForNewBranch(GitRepository repository, GitLocalBranch currentBranch) {
|
||||
Collection<GitRemote> remotes = repository.getRemotes();
|
||||
if (remotes.isEmpty()) {
|
||||
return null; // TODO need to propose to declare new remote
|
||||
}
|
||||
else if (remotes.size() == 1) {
|
||||
return makeTargetForNewBranch(repository, remotes.iterator().next(), currentBranch);
|
||||
}
|
||||
else {
|
||||
GitRemote remote = GitUtil.getDefaultRemote(remotes);
|
||||
if (remote == null) {
|
||||
remote = remotes.iterator().next();
|
||||
}
|
||||
return makeTargetForNewBranch(repository, remote, currentBranch);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GitPushTarget makeTargetForNewBranch(@NotNull GitRepository repository,
|
||||
@NotNull GitRemote remote,
|
||||
@NotNull GitLocalBranch currentBranch) {
|
||||
GitRemoteBranch existingRemoteBranch = GitPushTarget.findRemoteBranch(repository, remote, currentBranch.getName());
|
||||
if (existingRemoteBranch != null) {
|
||||
return new GitPushTarget(existingRemoteBranch, false);
|
||||
}
|
||||
return new GitPushTarget(new GitStandardRemoteBranch(remote, currentBranch.getName(), GitBranch.DUMMY_HASH), true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitPushSource getSource(@NotNull GitRepository repository) {
|
||||
return new GitPushSource(repository.getCurrentBranch()); // TODO assert: detached head => not possible to push
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public RepositoryManager<GitRepository> getRepositoryManager() {
|
||||
return myRepositoryManager;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PushTargetPanel<GitPushTarget> createTargetPanel(@NotNull GitRepository repository, @Nullable GitPushTarget defaultTarget) {
|
||||
return new GitPushTargetPanel(repository, defaultTarget);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushTarget;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.GitStandardRemoteBranch;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.validators.GitRefNameValidator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Collection;
|
||||
|
||||
class GitPushTarget implements PushTarget {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushTarget.class);
|
||||
|
||||
@NotNull private final GitRemoteBranch myRemoteBranch;
|
||||
private final boolean myIsNewBranchCreated;
|
||||
|
||||
GitPushTarget(@NotNull GitRemoteBranch remoteBranch, boolean isNewBranchCreated) {
|
||||
myRemoteBranch = remoteBranch;
|
||||
myIsNewBranchCreated = isNewBranchCreated;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
GitRemoteBranch getBranch() {
|
||||
return myRemoteBranch;
|
||||
}
|
||||
|
||||
boolean isNewBranchCreated() {
|
||||
return myIsNewBranchCreated;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static GitPushTarget parse(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) throws
|
||||
ParseException {
|
||||
if (!GitRefNameValidator.getInstance().checkInput(branchName)) {
|
||||
throw new ParseException("Invalid destination branch name: " + branchName, -1);
|
||||
}
|
||||
|
||||
GitRemote remote = findRemote(repository.getRemotes(), remoteName);
|
||||
if (remote == null) {
|
||||
LOG.error("Remote [" + remoteName + "] is not found among " + repository.getRemotes());
|
||||
throw new ParseException("Invalid remote: " + remoteName, -1);
|
||||
}
|
||||
|
||||
GitRemoteBranch existingRemoteBranch = findRemoteBranch(repository, remote, branchName);
|
||||
if (existingRemoteBranch != null) {
|
||||
return new GitPushTarget(existingRemoteBranch, false);
|
||||
}
|
||||
GitRemoteBranch rb = new GitStandardRemoteBranch(remote, branchName, GitBranch.DUMMY_HASH);
|
||||
return new GitPushTarget(rb, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GitRemote findRemote(@NotNull Collection<GitRemote> remotes, @NotNull final String candidate) {
|
||||
return ContainerUtil.find(remotes, new Condition<GitRemote>() {
|
||||
@Override
|
||||
public boolean value(GitRemote remote) {
|
||||
return remote.getName().equals(candidate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static GitRemoteBranch findRemoteBranch(@NotNull GitRepository repository, @NotNull final GitRemote remote,
|
||||
@NotNull final String nameAtRemote) {
|
||||
return ContainerUtil.find(repository.getBranches().getRemoteBranches(), new Condition<GitRemoteBranch>() {
|
||||
@Override
|
||||
public boolean value(GitRemoteBranch remoteBranch) {
|
||||
return remoteBranch.getRemote().equals(remote) &&
|
||||
remoteBranch.getNameForRemoteOperations().equals(GitBranchUtil.stripRefsPrefix(nameAtRemote));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushTargetPanel;
|
||||
import com.intellij.dvcs.push.ui.PushTargetTextField;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.ui.ValidationInfo;
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory;
|
||||
import com.intellij.openapi.ui.popup.ListPopup;
|
||||
import com.intellij.openapi.ui.popup.PopupStep;
|
||||
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.ui.ClickListener;
|
||||
import com.intellij.ui.ColoredTreeCellRenderer;
|
||||
import com.intellij.ui.SimpleTextAttributes;
|
||||
import com.intellij.ui.awt.RelativePoint;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.ParseException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
class GitPushTargetPanel extends PushTargetPanel<GitPushTarget> {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPushTargetPanel.class);
|
||||
|
||||
private static final Comparator<GitRemoteBranch> REMOTE_BRANCH_COMPARATOR = new MyRemoteBranchComparator();
|
||||
public static final String SEPARATOR = " \u25BE ";
|
||||
|
||||
private final GitRepository myRepository;
|
||||
private final PushTargetTextField myTargetTextField;
|
||||
private final JLabel myRemoteLabel;
|
||||
|
||||
@Nullable private GitPushTarget myCurrentTarget;
|
||||
|
||||
public GitPushTargetPanel(@NotNull GitRepository repository, @Nullable GitPushTarget defaultTarget) {
|
||||
myRepository = repository;
|
||||
|
||||
myCurrentTarget = defaultTarget;
|
||||
|
||||
String initialBranch;
|
||||
String initialRemote;
|
||||
if (defaultTarget == null) {
|
||||
// TODO
|
||||
initialBranch = "";
|
||||
initialRemote = "No remotes";
|
||||
}
|
||||
else {
|
||||
initialBranch = getTextFieldText(defaultTarget);
|
||||
initialRemote = getRemoteLabelText(defaultTarget.getBranch().getRemote().getName());
|
||||
}
|
||||
|
||||
myTargetTextField = new PushTargetTextField(repository.getProject(), getTargetNames(myRepository), initialBranch);
|
||||
|
||||
myRemoteLabel = new JBLabel(initialRemote);
|
||||
new ClickListener() {
|
||||
@Override
|
||||
public boolean onClick(@NotNull MouseEvent event, int clickCount) {
|
||||
showRemoteSelector();
|
||||
return true;
|
||||
}
|
||||
}.installOn(myRemoteLabel);
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
setOpaque(false);
|
||||
add(myRemoteLabel, BorderLayout.WEST);
|
||||
add(myTargetTextField, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private void showRemoteSelector() {
|
||||
final List<String> remotes = ContainerUtil.map(myRepository.getRemotes(), new Function<GitRemote, String>() {
|
||||
@Override
|
||||
public String fun(GitRemote remote) {
|
||||
return remote.getName();
|
||||
}
|
||||
});
|
||||
|
||||
ListPopup popup = JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<String>(null, remotes) {
|
||||
@Override
|
||||
public PopupStep onChosen(String selectedValue, boolean finalChoice) {
|
||||
myRemoteLabel.setText(getRemoteLabelText(selectedValue));
|
||||
return super.onChosen(selectedValue, finalChoice);
|
||||
}
|
||||
});
|
||||
// underneath, but so that list popup items are aligned with the label
|
||||
popup.show(new RelativePoint(myRemoteLabel, new Point(-UIUtil.getListCellHPadding(), myRemoteLabel.getHeight())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NotNull ColoredTreeCellRenderer renderer) {
|
||||
String targetName = myTargetTextField.getText();
|
||||
if (StringUtil.isEmptyOrSpaces(targetName)) {
|
||||
renderer.append("Specify remote branch to push", SimpleTextAttributes.ERROR_ATTRIBUTES, this);
|
||||
}
|
||||
else {
|
||||
GitPushTarget target = getValue();
|
||||
renderer.append(myRemoteLabel.getText(), SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
|
||||
if (target.isNewBranchCreated()) {
|
||||
renderer.append("+", SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
|
||||
}
|
||||
renderer.append(target.getBranch().getNameForRemoteOperations() + SEPARATOR, SimpleTextAttributes.SYNTHETIC_ATTRIBUTES, this);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitPushTarget getValue() {
|
||||
return ObjectUtils.assertNotNull(myCurrentTarget);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getTextFieldText(@Nullable GitPushTarget target) {
|
||||
return (target != null ? target.getBranch().getNameForRemoteOperations() : "");
|
||||
}
|
||||
|
||||
private static String getRemoteLabelText(@NotNull String selectedValue) {
|
||||
return selectedValue + SEPARATOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireOnCancel() {
|
||||
myTargetTextField.setText(getTextFieldText(myCurrentTarget));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireOnChange() {
|
||||
String remoteName = myRemoteLabel.getText().replace(SEPARATOR, "");
|
||||
String branchName = myTargetTextField.getText();
|
||||
try {
|
||||
myCurrentTarget = GitPushTarget.parse(myRepository, remoteName, branchName);
|
||||
}
|
||||
catch (ParseException e) {
|
||||
LOG.error("Invalid remote name shouldn't be allowed. [" + remoteName + ", " + branchName + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ValidationInfo verify() {
|
||||
try {
|
||||
String remoteLabel = myRemoteLabel.getText().replace(SEPARATOR, "");
|
||||
GitPushTarget.parse(myRepository, remoteLabel, myTargetTextField.getText());
|
||||
return null;
|
||||
}
|
||||
catch (ParseException e) {
|
||||
return new ValidationInfo(e.getMessage(), myTargetTextField);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getTargetNames(@NotNull GitRepository repository) {
|
||||
List<GitRemoteBranch> remoteBranches = ContainerUtil.sorted(repository.getBranches().getRemoteBranches(), REMOTE_BRANCH_COMPARATOR);
|
||||
return ContainerUtil.map(remoteBranches, new Function<GitRemoteBranch, String>() {
|
||||
@Override
|
||||
public String fun(GitRemoteBranch branch) {
|
||||
return branch.getNameForRemoteOperations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class MyRemoteBranchComparator implements Comparator<GitRemoteBranch> {
|
||||
@Override
|
||||
public int compare(GitRemoteBranch o1, GitRemoteBranch o2) {
|
||||
String remoteName1 = o1.getRemote().getName();
|
||||
String remoteName2 = o2.getRemote().getName();
|
||||
int remoteComparison = remoteName1.compareTo(remoteName2);
|
||||
if (remoteComparison != 0) {
|
||||
if (remoteName1.equals(GitRemote.ORIGIN_NAME)) {
|
||||
return -1;
|
||||
}
|
||||
if (remoteName2.equals(GitRemote.ORIGIN_NAME)) {
|
||||
return 1;
|
||||
}
|
||||
return remoteComparison;
|
||||
}
|
||||
return o1.getNameForLocalOperations().compareTo(o2.getNameForLocalOperations());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,549 +15,33 @@
|
||||
*/
|
||||
package git4idea.push;
|
||||
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import com.intellij.openapi.progress.Task;
|
||||
import com.intellij.dvcs.push.PushSpec;
|
||||
import com.intellij.dvcs.push.Pusher;
|
||||
import com.intellij.dvcs.push.VcsPushOptionValue;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.VcsNotifier;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import git4idea.*;
|
||||
import git4idea.branch.GitBranchPair;
|
||||
import git4idea.commands.Git;
|
||||
import git4idea.commands.GitCommandResult;
|
||||
import git4idea.commands.GitLineHandlerListener;
|
||||
import git4idea.commands.GitStandardProgressAnalyzer;
|
||||
import git4idea.config.GitConfigUtil;
|
||||
import git4idea.config.GitVcsSettings;
|
||||
import git4idea.config.UpdateMethod;
|
||||
import git4idea.history.GitHistoryUtils;
|
||||
import git4idea.repo.GitBranchTrackInfo;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import git4idea.settings.GitPushSettings;
|
||||
import git4idea.update.GitUpdateProcess;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Collects information to push and performs the push.
|
||||
*/
|
||||
public final class GitPusher {
|
||||
|
||||
/**
|
||||
* if diff-log is not available (new branch is created, for example), we show a few recent commits made on the branch
|
||||
*/
|
||||
static final int RECENT_COMMITS_NUMBER = 5;
|
||||
|
||||
@Deprecated
|
||||
static final GitRemoteBranch NO_TARGET_BRANCH = new GitStandardRemoteBranch(GitRemote.DOT, "", GitBranch.DUMMY_HASH);
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPusher.class);
|
||||
private static final String INDICATOR_TEXT = "Pushing";
|
||||
private static final int MAX_PUSH_ATTEMPTS = 10;
|
||||
class GitPusher extends Pusher<GitRepository, GitPushSource, GitPushTarget> {
|
||||
|
||||
@NotNull private final Project myProject;
|
||||
@NotNull private final GitRepositoryManager myRepositoryManager;
|
||||
@NotNull private final ProgressIndicator myProgressIndicator;
|
||||
@NotNull private final Collection<GitRepository> myRepositories;
|
||||
@NotNull private final GitVcsSettings mySettings;
|
||||
@NotNull private final GitPushSettings myPushSettings;
|
||||
@NotNull private final Git myGit;
|
||||
@NotNull private final GitPlatformFacade myPlatformFacade;
|
||||
|
||||
public static void showPushDialogAndPerformPush(@NotNull Project project, @NotNull GitPlatformFacade facade) {
|
||||
final GitPushDialog dialog = new GitPushDialog(project);
|
||||
dialog.show();
|
||||
if (dialog.isOK()) {
|
||||
runPushInBackground(project, facade, dialog);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runPushInBackground(@NotNull final Project project, @NotNull final GitPlatformFacade facade,
|
||||
@NotNull final GitPushDialog dialog) {
|
||||
Task.Backgroundable task = new Task.Backgroundable(project, INDICATOR_TEXT, false) {
|
||||
@Override
|
||||
public void run(@NotNull ProgressIndicator indicator) {
|
||||
new GitPusher(project, facade, indicator).push(dialog.getPushInfo());
|
||||
}
|
||||
};
|
||||
GitVcs.runInBackground(task);
|
||||
}
|
||||
|
||||
// holds settings chosen in GitRejectedPushUpdate dialog to reuse if the next push is rejected again.
|
||||
static class UpdateSettings {
|
||||
private final boolean myUpdateAllRoots;
|
||||
private final UpdateMethod myUpdateMethod;
|
||||
|
||||
private UpdateSettings(boolean updateAllRoots, UpdateMethod updateMethod) {
|
||||
myUpdateAllRoots = updateAllRoots;
|
||||
myUpdateMethod = updateMethod;
|
||||
}
|
||||
|
||||
public boolean shouldUpdateAllRoots() {
|
||||
return myUpdateAllRoots;
|
||||
}
|
||||
|
||||
public UpdateMethod getUpdateMethod() {
|
||||
return myUpdateMethod;
|
||||
}
|
||||
|
||||
public boolean shouldUpdate() {
|
||||
return getUpdateMethod() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("UpdateSettings{myUpdateAllRoots=%s, myUpdateMethod=%s}", myUpdateAllRoots, myUpdateMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public GitPusher(@NotNull Project project, @NotNull GitPlatformFacade facade, @NotNull ProgressIndicator indicator) {
|
||||
GitPusher(@NotNull Project project) {
|
||||
myProject = project;
|
||||
myPlatformFacade = facade;
|
||||
myProgressIndicator = indicator;
|
||||
myRepositoryManager = GitUtil.getRepositoryManager(myProject);
|
||||
myRepositories = myRepositoryManager.getRepositories();
|
||||
mySettings = GitVcsSettings.getInstance(myProject);
|
||||
myPushSettings = GitPushSettings.getInstance(myProject);
|
||||
myGit = ServiceManager.getService(Git.class);
|
||||
myRepositoryManager = GitUtil.getRepositoryManager(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pushSpecs which branches in which repositories should be pushed.
|
||||
* The most common situation is all repositories in the project with a single currently active branch for
|
||||
* each of them.
|
||||
* @throws VcsException if couldn't query 'git log' about commits to be pushed.
|
||||
* @return
|
||||
*/
|
||||
@NotNull
|
||||
GitCommitsByRepoAndBranch collectCommitsToPush(@NotNull Map<GitRepository, GitPushSpec> pushSpecs) throws VcsException {
|
||||
Map<GitRepository, List<GitBranchPair>> reposAndBranchesToPush = prepareRepositoriesAndBranchesToPush(pushSpecs);
|
||||
|
||||
Map<GitRepository, GitCommitsByBranch> commitsByRepoAndBranch = new HashMap<GitRepository, GitCommitsByBranch>();
|
||||
for (GitRepository repository : myRepositories) {
|
||||
List<GitBranchPair> branchPairs = reposAndBranchesToPush.get(repository);
|
||||
if (branchPairs == null) {
|
||||
continue;
|
||||
}
|
||||
GitCommitsByBranch commitsByBranch = collectsCommitsToPush(repository, branchPairs);
|
||||
commitsByRepoAndBranch.put(repository, commitsByBranch);
|
||||
}
|
||||
return new GitCommitsByRepoAndBranch(commitsByRepoAndBranch);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<GitRepository, List<GitBranchPair>> prepareRepositoriesAndBranchesToPush(@NotNull Map<GitRepository, GitPushSpec> pushSpecs) throws VcsException {
|
||||
Map<GitRepository, List<GitBranchPair>> res = new HashMap<GitRepository, List<GitBranchPair>>();
|
||||
for (GitRepository repository : myRepositories) {
|
||||
GitPushSpec pushSpec = pushSpecs.get(repository);
|
||||
if (pushSpec == null) {
|
||||
continue;
|
||||
}
|
||||
res.put(repository, Collections.singletonList(new GitBranchPair(pushSpec.getSource(), pushSpec.getDest())));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitCommitsByBranch collectsCommitsToPush(@NotNull GitRepository repository, @NotNull List<GitBranchPair> sourcesDestinations)
|
||||
throws VcsException {
|
||||
Map<GitBranch, GitPushBranchInfo> commitsByBranch = new HashMap<GitBranch, GitPushBranchInfo>();
|
||||
|
||||
for (GitBranchPair sourceDest : sourcesDestinations) {
|
||||
GitLocalBranch source = sourceDest.getBranch();
|
||||
GitRemoteBranch dest = sourceDest.getDest();
|
||||
assert dest != null : "Destination branch can't be null here for branch " + source;
|
||||
|
||||
List<GitCommit> commits;
|
||||
GitPushBranchInfo.Type type;
|
||||
if (dest == NO_TARGET_BRANCH) {
|
||||
commits = collectRecentCommitsOnBranch(repository, source);
|
||||
type = GitPushBranchInfo.Type.NO_TRACKED_OR_TARGET;
|
||||
}
|
||||
else if (GitUtil.repoContainsRemoteBranch(repository, dest)) {
|
||||
commits = collectCommitsToPush(repository, source.getName(), dest.getName());
|
||||
type = GitPushBranchInfo.Type.STANDARD;
|
||||
}
|
||||
else {
|
||||
commits = collectRecentCommitsOnBranch(repository, source);
|
||||
type = GitPushBranchInfo.Type.NEW_BRANCH;
|
||||
}
|
||||
commitsByBranch.put(source, new GitPushBranchInfo(source, dest, commits, type));
|
||||
}
|
||||
|
||||
return new GitCommitsByBranch(commitsByBranch);
|
||||
}
|
||||
|
||||
private List<GitCommit> collectRecentCommitsOnBranch(GitRepository repository, GitBranch source) throws VcsException {
|
||||
return GitHistoryUtils.history(myProject, repository.getRoot(), "--max-count=" + RECENT_COMMITS_NUMBER, source.getName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<GitCommit> collectCommitsToPush(@NotNull GitRepository repository, @NotNull String source, @NotNull String destination)
|
||||
throws VcsException {
|
||||
return GitHistoryUtils.history(myProject, repository.getRoot(), destination + ".." + source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes push, shows the result in a notification. If push for current branch is rejected, shows a dialog proposing to update.
|
||||
*/
|
||||
public void push(@NotNull GitPushInfo pushInfo) {
|
||||
push(pushInfo, null, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes push, shows the result in a notification. If push for current branch is rejected, shows a dialog proposing to update.
|
||||
* If {@code previousResult} and {@code updateSettings} are set, it means that this push is not the first, but is after a successful update.
|
||||
* In that case, if push is rejected again, the dialog is not shown, and update is performed automatically with the previously chosen
|
||||
* option.
|
||||
* Also, at the end results are merged and are shown in a single notification.
|
||||
*/
|
||||
private void push(@NotNull GitPushInfo pushInfo, @Nullable GitPushResult previousResult, @Nullable UpdateSettings updateSettings, int attempt) {
|
||||
GitPushResult result = tryPushAndGetResult(pushInfo);
|
||||
handleResult(pushInfo, result, previousResult, updateSettings, attempt);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitPushResult tryPushAndGetResult(@NotNull GitPushInfo pushInfo) {
|
||||
GitPushResult pushResult = new GitPushResult(myProject);
|
||||
|
||||
GitCommitsByRepoAndBranch commits = pushInfo.getCommits();
|
||||
for (GitRepository repository : commits.getRepositories()) {
|
||||
if (commits.get(repository).getAllCommits().size() == 0) { // don't push repositories where there is nothing to push. Note that when a branch is created, several recent commits are stored in the pushInfo.
|
||||
continue;
|
||||
}
|
||||
GitPushRepoResult repoResult = pushRepository(pushInfo, commits, repository);
|
||||
if (repoResult.getType() == GitPushRepoResult.Type.NOT_PUSHING) {
|
||||
continue;
|
||||
}
|
||||
pushResult.append(repository, repoResult);
|
||||
GitPushRepoResult.Type resultType = repoResult.getType();
|
||||
if (resultType == GitPushRepoResult.Type.CANCEL || resultType == GitPushRepoResult.Type.NOT_AUTHORIZED) { // don't proceed if user has cancelled or couldn't login
|
||||
break;
|
||||
}
|
||||
}
|
||||
myRepositoryManager.updateAllRepositories(); // new remote branch may be created
|
||||
return pushResult;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitPushRepoResult pushRepository(@NotNull GitPushInfo pushInfo,
|
||||
@NotNull GitCommitsByRepoAndBranch commits,
|
||||
@NotNull GitRepository repository) {
|
||||
GitPushSpec pushSpec = pushInfo.getPushSpecs().get(repository);
|
||||
GitSimplePushResult simplePushResult = pushAndGetSimpleResult(repository, pushSpec);
|
||||
String output = simplePushResult.getOutput();
|
||||
switch (simplePushResult.getType()) {
|
||||
case SUCCESS:
|
||||
return successOrErrorRepoResult(commits, repository, output, true);
|
||||
case ERROR:
|
||||
return successOrErrorRepoResult(commits, repository, output, false);
|
||||
case REJECT:
|
||||
return getResultFromRejectedPush(commits, repository, simplePushResult);
|
||||
case NOT_AUTHORIZED:
|
||||
return GitPushRepoResult.notAuthorized(output);
|
||||
case CANCEL:
|
||||
return GitPushRepoResult.cancelled(output);
|
||||
case NOT_PUSHED:
|
||||
return GitPushRepoResult.notPushed();
|
||||
default:
|
||||
return GitPushRepoResult.cancelled(output);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GitPushRepoResult getResultFromRejectedPush(@NotNull GitCommitsByRepoAndBranch commits,
|
||||
@NotNull GitRepository repository,
|
||||
@NotNull GitSimplePushResult simplePushResult) {
|
||||
Collection<String> rejectedBranches = simplePushResult.getRejectedBranches();
|
||||
|
||||
Map<GitBranch, GitPushBranchResult> resultMap = new HashMap<GitBranch, GitPushBranchResult>();
|
||||
GitCommitsByBranch commitsByBranch = commits.get(repository);
|
||||
boolean pushedBranchWasRejected = false;
|
||||
for (GitBranch branch : commitsByBranch.getBranches()) {
|
||||
GitPushBranchResult branchResult;
|
||||
if (branchInRejected(branch, rejectedBranches)) {
|
||||
branchResult = GitPushBranchResult.rejected();
|
||||
pushedBranchWasRejected = true;
|
||||
}
|
||||
else {
|
||||
branchResult = successfulResultForBranch(commitsByBranch, branch);
|
||||
}
|
||||
resultMap.put(branch, branchResult);
|
||||
}
|
||||
|
||||
if (pushedBranchWasRejected) {
|
||||
return GitPushRepoResult.someRejected(resultMap, simplePushResult.getOutput());
|
||||
} else {
|
||||
// The rejectedDetector detected rejected push of the branch which had nothing to push (but is behind the upstream). We are not counting it.
|
||||
return GitPushRepoResult.success(resultMap, simplePushResult.getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitSimplePushResult pushAndGetSimpleResult(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec) {
|
||||
if (pushSpec.getDest() == NO_TARGET_BRANCH) {
|
||||
return GitSimplePushResult.notPushed();
|
||||
}
|
||||
|
||||
GitRemote remote = pushSpec.getRemote();
|
||||
Collection<String> pushUrls = remote.getPushUrls();
|
||||
if (pushUrls.isEmpty()) {
|
||||
LOG.error("No urls or pushUrls are defined for " + remote);
|
||||
return GitSimplePushResult.error("There are no URLs defined for remote " + remote.getName());
|
||||
}
|
||||
String url = pushUrls.iterator().next();
|
||||
GitSimplePushResult pushResult;
|
||||
pushResult = pushNatively(repository, pushSpec, url);
|
||||
|
||||
if (pushResult.getType() == GitSimplePushResult.Type.SUCCESS) {
|
||||
setUpstream(repository, pushSpec.getSource(), pushSpec.getRemote(), pushSpec.getDest());
|
||||
}
|
||||
|
||||
return pushResult;
|
||||
}
|
||||
|
||||
private static void setUpstream(@NotNull GitRepository repository,
|
||||
@NotNull GitLocalBranch source, @NotNull GitRemote remote, @NotNull GitRemoteBranch dest) {
|
||||
if (!branchTrackingInfoIsSet(repository, source)) {
|
||||
Project project = repository.getProject();
|
||||
VirtualFile root = repository.getRoot();
|
||||
String branchName = source.getName();
|
||||
try {
|
||||
boolean rebase = getMergeOrRebaseConfig(project, root);
|
||||
GitConfigUtil.setValue(project, root, "branch." + branchName + ".remote", remote.getName());
|
||||
GitConfigUtil.setValue(project, root, "branch." + branchName + ".merge",
|
||||
GitBranch.REFS_HEADS_PREFIX + dest.getNameForRemoteOperations());
|
||||
if (rebase) {
|
||||
GitConfigUtil.setValue(project, root, "branch." + branchName + ".rebase", "true");
|
||||
}
|
||||
}
|
||||
catch (VcsException e) {
|
||||
LOG.error(String.format("Couldn't set up tracking for source branch %s, target branch %s, remote %s in root %s",
|
||||
source, dest, remote, repository), e);
|
||||
VcsNotifier.getInstance(project).notifyWeakError("Couldn't set up branch tracking");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean getMergeOrRebaseConfig(Project project, VirtualFile root) throws VcsException {
|
||||
String autoSetupRebase = GitConfigUtil.getValue(project, root, GitConfigUtil.BRANCH_AUTOSETUP_REBASE);
|
||||
return autoSetupRebase != null && (autoSetupRebase.equals("remote") || autoSetupRebase.equals("always"));
|
||||
}
|
||||
|
||||
private static boolean branchTrackingInfoIsSet(@NotNull GitRepository repository, @NotNull GitLocalBranch source) {
|
||||
for (GitBranchTrackInfo trackInfo : repository.getBranchTrackInfos()) {
|
||||
if (trackInfo.getLocalBranch().equals(source)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitSimplePushResult pushNatively(GitRepository repository, GitPushSpec pushSpec, @NotNull String url) {
|
||||
GitPushRejectedDetector rejectedDetector = new GitPushRejectedDetector();
|
||||
GitLineHandlerListener progressListener = GitStandardProgressAnalyzer.createListener(myProgressIndicator);
|
||||
GitCommandResult res = myGit.push(repository, pushSpec, url, rejectedDetector, progressListener);
|
||||
if (rejectedDetector.rejected()) {
|
||||
Collection<String> rejectedBranches = rejectedDetector.getRejectedBranches();
|
||||
return GitSimplePushResult.reject(rejectedBranches);
|
||||
}
|
||||
else if (res.success()) {
|
||||
return GitSimplePushResult.success();
|
||||
}
|
||||
else {
|
||||
return GitSimplePushResult.error(res.getErrorOutputAsHtmlString());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GitPushRepoResult successOrErrorRepoResult(@NotNull GitCommitsByRepoAndBranch commits, @NotNull GitRepository repository, @NotNull String output, boolean success) {
|
||||
GitPushRepoResult repoResult;
|
||||
Map<GitBranch, GitPushBranchResult> resultMap = new HashMap<GitBranch, GitPushBranchResult>();
|
||||
GitCommitsByBranch commitsByBranch = commits.get(repository);
|
||||
for (GitBranch branch : commitsByBranch.getBranches()) {
|
||||
GitPushBranchResult branchResult = success ?
|
||||
successfulResultForBranch(commitsByBranch, branch) :
|
||||
GitPushBranchResult.error();
|
||||
resultMap.put(branch, branchResult);
|
||||
}
|
||||
repoResult = success ? GitPushRepoResult.success(resultMap, output) : GitPushRepoResult.error(resultMap, output);
|
||||
return repoResult;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static GitPushBranchResult successfulResultForBranch(@NotNull GitCommitsByBranch commitsByBranch, @NotNull GitBranch branch) {
|
||||
GitPushBranchInfo branchInfo = commitsByBranch.get(branch);
|
||||
if (branchInfo.isNewBranchCreated()) {
|
||||
return GitPushBranchResult.newBranch(branchInfo.getDestBranch().getName());
|
||||
}
|
||||
return GitPushBranchResult.success(branchInfo.getCommits().size());
|
||||
}
|
||||
|
||||
private static boolean branchInRejected(@NotNull GitBranch branch, @NotNull Collection<String> rejectedBranches) {
|
||||
String branchName = branch.getName();
|
||||
final String REFS_HEADS = "refs/heads/";
|
||||
if (branchName.startsWith(REFS_HEADS)) {
|
||||
branchName = branchName.substring(REFS_HEADS.length());
|
||||
}
|
||||
|
||||
for (String rejectedBranch : rejectedBranches) {
|
||||
if (rejectedBranch.equals(branchName) || (rejectedBranch.startsWith(REFS_HEADS) && rejectedBranch.substring(REFS_HEADS.length()).equals(branchName))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// if all repos succeeded, show notification.
|
||||
// if all repos failed, show notification.
|
||||
// if some repos failed, show notification with both results.
|
||||
// if in failed repos, some branches were rejected, it is a warning-type, but not an error.
|
||||
// if in a failed repo, current branch was rejected, propose to update the branch. Don't do it if not current branch was rejected,
|
||||
// since it is difficult to update such a branch.
|
||||
// if in a failed repo, a branch was rejected that had nothing to push, don't notify about the rejection.
|
||||
// Besides all of the above, don't confuse users with 1 repository with all this "repository/root" stuff;
|
||||
// don't confuse users which push only a single branch with all this "branch" stuff.
|
||||
private void handleResult(@NotNull GitPushInfo pushInfo, @NotNull GitPushResult result, @Nullable GitPushResult previousResult, @Nullable UpdateSettings updateSettings,
|
||||
int pushAttempt) {
|
||||
result.mergeFrom(previousResult);
|
||||
|
||||
if (result.isEmpty()) {
|
||||
VcsNotifier.getInstance(myProject).notifyInfo("Nothing to push");
|
||||
}
|
||||
else if (result.wasErrorCancelOrNotAuthorized()) {
|
||||
// if there was an error on any repo, we won't propose to update even if current branch of a repo was rejected
|
||||
result.createPushNotificationAndNotify();
|
||||
}
|
||||
else {
|
||||
// there were no errors, but there might be some rejected branches on some of the repositories
|
||||
// => for current branch propose to update and re-push it. For others just warn
|
||||
Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch = result.getRejectedPushesFromCurrentBranchToTrackedBranch(pushInfo);
|
||||
|
||||
if (pushAttempt <= MAX_PUSH_ATTEMPTS && !rejectedPushesForCurrentBranch.isEmpty()) {
|
||||
|
||||
LOG.info(
|
||||
String.format("Rejected pushes for current branches: %n%s%nUpdate settings: %s", rejectedPushesForCurrentBranch, updateSettings));
|
||||
|
||||
if (updateSettings == null) {
|
||||
// show dialog only when push is rejected for the first time in a row, otherwise reuse previously chosen update method
|
||||
// and don't show the dialog again if user has chosen not to ask again
|
||||
updateSettings = readUpdateSettings();
|
||||
if (!mySettings.autoUpdateIfPushRejected()) {
|
||||
updateSettings = showDialogAndGetExitCode(rejectedPushesForCurrentBranch, updateSettings);
|
||||
saveUpdateSettings(updateSettings);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateSettings.shouldUpdate()) {
|
||||
Collection<GitRepository> repositoriesToUpdate = getRootsToUpdate(rejectedPushesForCurrentBranch, updateSettings.shouldUpdateAllRoots());
|
||||
GitPushResult adjustedPushResult = result.remove(rejectedPushesForCurrentBranch);
|
||||
adjustedPushResult.markUpdateStartIfNotMarked(repositoriesToUpdate);
|
||||
boolean updateResult = update(repositoriesToUpdate, updateSettings.getUpdateMethod());
|
||||
if (updateResult) {
|
||||
myProgressIndicator.setText(INDICATOR_TEXT);
|
||||
GitPushInfo newPushInfo = pushInfo.retain(rejectedPushesForCurrentBranch);
|
||||
push(newPushInfo, adjustedPushResult, updateSettings, pushAttempt + 1);
|
||||
return; // don't notify - next push will notify all results in compound
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
result.createPushNotificationAndNotify();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveUpdateSettings(@NotNull UpdateSettings updateSettings) {
|
||||
UpdateMethod updateMethod = updateSettings.getUpdateMethod();
|
||||
if (updateMethod != null) { // null if user has pressed cancel
|
||||
myPushSettings.setUpdateAllRoots(updateSettings.shouldUpdateAllRoots());
|
||||
myPushSettings.setUpdateMethod(updateMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private UpdateSettings readUpdateSettings() {
|
||||
boolean updateAllRoots = myPushSettings.shouldUpdateAllRoots();
|
||||
UpdateMethod updateMethod = myPushSettings.getUpdateMethod();
|
||||
return new UpdateSettings(updateAllRoots, updateMethod);
|
||||
}
|
||||
|
||||
private UpdateSettings showDialogAndGetExitCode(final Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch,
|
||||
final UpdateSettings initialSettings) {
|
||||
final AtomicReference<UpdateSettings> updateSettings = new AtomicReference<UpdateSettings>();
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final GitRejectedPushUpdateDialog dialog = new GitRejectedPushUpdateDialog(myProject, rejectedPushesForCurrentBranch.keySet(), initialSettings);
|
||||
dialog.show();
|
||||
final int exitCode = dialog.getExitCode();
|
||||
if (exitCode != DialogWrapper.CANCEL_EXIT_CODE) {
|
||||
mySettings.setAutoUpdateIfPushRejected(dialog.shouldAutoUpdateInFuture());
|
||||
}
|
||||
updateSettings.set(new UpdateSettings(dialog.shouldUpdateAll(), getUpdateMethodFromDialogExitCode(exitCode)));
|
||||
|
||||
}
|
||||
});
|
||||
return updateSettings.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return update method selected in the dialog or {@code null} if user pressed Cancel, i.e. doesn't want to update.
|
||||
*/
|
||||
@Nullable
|
||||
private static UpdateMethod getUpdateMethodFromDialogExitCode(int exitCode) {
|
||||
switch (exitCode) {
|
||||
case GitRejectedPushUpdateDialog.MERGE_EXIT_CODE: return UpdateMethod.MERGE;
|
||||
case GitRejectedPushUpdateDialog.REBASE_EXIT_CODE: return UpdateMethod.REBASE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Collection<GitRepository> getRootsToUpdate(@NotNull Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch, boolean updateAllRoots) {
|
||||
return updateAllRoots ? myRepositories : rejectedPushesForCurrentBranch.keySet();
|
||||
}
|
||||
|
||||
private boolean update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
|
||||
GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ? GitUpdateProcess.UpdateMethod.MERGE : GitUpdateProcess.UpdateMethod.REBASE;
|
||||
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator,
|
||||
new HashSet<GitRepository>(rootsToUpdate), UpdatedFiles.create()).update(um);
|
||||
for (GitRepository repository : rootsToUpdate) {
|
||||
repository.getRoot().refresh(true, true);
|
||||
}
|
||||
if (updateResult == GitUpdateResult.SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
else if (updateResult == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS || updateResult == GitUpdateResult.INCOMPLETE) {
|
||||
String title = "Push cancelled";
|
||||
String description;
|
||||
if (updateResult == GitUpdateResult.INCOMPLETE) {
|
||||
description = "Push has been cancelled, because not all conflicts were resolved during update.<br/>" +
|
||||
"Resolve the conflicts and invoke push again.";
|
||||
}
|
||||
else {
|
||||
description = "Push has been cancelled, because there were conflicts during update.<br/>" +
|
||||
"Check that conflicts were resolved correctly, and invoke push again.";
|
||||
}
|
||||
VcsNotifier.getInstance(myProject).notifyMinorWarning(title, description);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public void push(@NotNull Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> pushSpecs, @Nullable VcsPushOptionValue vcsPushOptionValue, boolean force) {
|
||||
GitPushResult result = new GitPushOperation(myProject, pushSpecs, force).execute();
|
||||
GitPushResultNotification notification = GitPushResultNotification.create(myProject, result, myRepositoryManager.moreThanOneRoot());
|
||||
notification.notify(myProject);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class GitRejectedPushUpdateDialog extends DialogWrapper {
|
||||
|
||||
protected GitRejectedPushUpdateDialog(@NotNull Project project,
|
||||
@NotNull Collection<GitRepository> repositories,
|
||||
@NotNull GitPusher.UpdateSettings initialSettings) {
|
||||
@NotNull PushUpdateSettings initialSettings) {
|
||||
super(project);
|
||||
myProject = project;
|
||||
myRepositories = repositories;
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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.push;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public final class GitSimplePushResult {
|
||||
|
||||
private final Type myType;
|
||||
private final String myErrorOutput;
|
||||
private final Collection<String> myRejectedBranches;
|
||||
|
||||
public enum Type {
|
||||
NOT_PUSHED,
|
||||
SUCCESS,
|
||||
REJECT,
|
||||
CANCEL,
|
||||
NOT_AUTHORIZED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private GitSimplePushResult(@NotNull Type type, @NotNull String errorOutput, @NotNull Collection<String> rejectedBranches) {
|
||||
myType = type;
|
||||
myErrorOutput = errorOutput;
|
||||
myRejectedBranches = rejectedBranches;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult success() {
|
||||
return new GitSimplePushResult(Type.SUCCESS, "", Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult notPushed() {
|
||||
return new GitSimplePushResult(Type.NOT_PUSHED, "", Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult cancel() {
|
||||
return new GitSimplePushResult(Type.CANCEL, "Cancelled by user", Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult notAuthorized() {
|
||||
return new GitSimplePushResult(Type.NOT_AUTHORIZED, "Couldn't authorize", Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult reject(@NotNull Collection<String> rejectedBranches) {
|
||||
return new GitSimplePushResult(Type.REJECT, "", rejectedBranches);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitSimplePushResult error(@NotNull String errorOutput) {
|
||||
return new GitSimplePushResult(Type.ERROR, errorOutput, Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getOutput() {
|
||||
return myErrorOutput;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<String> getRejectedBranches() {
|
||||
return myRejectedBranches;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Type getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.repo.GitRepository;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
class GroupedPushResult {
|
||||
|
||||
final Map<GitRepository, GitPushRepoResult> successful;
|
||||
final Map<GitRepository, GitPushRepoResult> errors;
|
||||
final Map<GitRepository, GitPushRepoResult> rejected;
|
||||
|
||||
private GroupedPushResult(Map<GitRepository, GitPushRepoResult> successful,
|
||||
Map<GitRepository, GitPushRepoResult> errors,
|
||||
Map<GitRepository, GitPushRepoResult> rejected) {
|
||||
this.successful = successful;
|
||||
this.errors = errors;
|
||||
this.rejected = rejected;
|
||||
}
|
||||
|
||||
static GroupedPushResult group(Map<GitRepository, GitPushRepoResult> results) {
|
||||
Map<GitRepository, GitPushRepoResult> successful = ContainerUtil.newHashMap();
|
||||
Map<GitRepository, GitPushRepoResult> rejected = ContainerUtil.newHashMap();
|
||||
Map<GitRepository, GitPushRepoResult> errors = ContainerUtil.newHashMap();
|
||||
for (Map.Entry<GitRepository, GitPushRepoResult> entry : results.entrySet()) {
|
||||
GitRepository repository = entry.getKey();
|
||||
GitPushRepoResult result = entry.getValue();
|
||||
|
||||
if (result.getType() == GitPushRepoResult.Type.REJECTED) {
|
||||
rejected.put(repository, result);
|
||||
}
|
||||
else if (result.getType() == GitPushRepoResult.Type.ERROR) {
|
||||
errors.put(repository, result);
|
||||
}
|
||||
else {
|
||||
successful.put(repository, result);
|
||||
}
|
||||
}
|
||||
return new GroupedPushResult(successful, errors, rejected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import git4idea.config.UpdateMethod;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// holds settings chosen in GitRejectedPushUpdate dialog to reuse if the next push is rejected again.
|
||||
class PushUpdateSettings {
|
||||
|
||||
private final boolean myUpdateAllRoots;
|
||||
@NotNull private final UpdateMethod myUpdateMethod;
|
||||
|
||||
PushUpdateSettings(boolean updateAllRoots, @NotNull UpdateMethod updateMethod) {
|
||||
myUpdateAllRoots = updateAllRoots;
|
||||
myUpdateMethod = updateMethod;
|
||||
}
|
||||
|
||||
boolean shouldUpdateAllRoots() {
|
||||
return myUpdateAllRoots;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
UpdateMethod getUpdateMethod() {
|
||||
return myUpdateMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("UpdateSettings{myUpdateAllRoots=%s, myUpdateMethod=%s}", myUpdateAllRoots, myUpdateMethod);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GitPushNativeResultParserTest {
|
||||
|
||||
public static final String STANDARD_PREFIX = "Counting objects: 20, done.\n" +
|
||||
"Delta compression using up to 8 threads.\n" +
|
||||
"Compressing objects: 100% (14/14), done.\n" +
|
||||
"Writing objects: 100% (20/20), 1.70 KiB | 0 bytes/s, done.\n" +
|
||||
"Total 20 (delta 7), reused 0 (delta 0)\n" +
|
||||
"To /Users/loki/sandbox/git/parent.git";
|
||||
public static final String TARGET_PREFIX = "To http://example.com/git/parent.git";
|
||||
public static final String PUSH_DEFAULT_WARNING = "warning: push.default is unset; its implicit value has changed in\n" +
|
||||
"Git 2.0 from 'matching' to 'simple'. To squelch this message\n" +
|
||||
"and maintain the traditional behavior, use:\n" +
|
||||
"\n" +
|
||||
" git config --global push.default matching\n" +
|
||||
"\n" +
|
||||
"To squelch this message and adopt the new behavior now, use:\n" +
|
||||
"\n" +
|
||||
" git config --global push.default simple\n" +
|
||||
"\n" +
|
||||
"When push.default is set to 'matching', git will push local branches\n" +
|
||||
"to the remote branches that already exist with the same name.\n" +
|
||||
"\n" +
|
||||
"Since Git 2.0, Git defaults to the more conservative 'simple'\n" +
|
||||
"behavior, which only pushes the current branch to the corresponding\n" +
|
||||
"remote branch that 'git pull' uses to update the current branch.\n" +
|
||||
"\n" +
|
||||
"See 'git help config' and search for 'push.default' for further information.\n" +
|
||||
"(the 'simple' mode was introduced in Git 1.7.11. Use the similar mode\n" +
|
||||
"'current' instead of 'simple' if you sometimes use older versions of Git)";
|
||||
public static final String SUCCESS_SUFFIX = "Done";
|
||||
public static final String REJECT_SUFFIX = "Done\n" +
|
||||
"error: failed to push some refs to '/Users/loki/sandbox/git/parent.git'\n" +
|
||||
"hint: Updates were rejected because the tip of your current branch is behind\n" +
|
||||
"hint: its remote counterpart. Integrate the remote changes (e.g.\n" +
|
||||
"hint: 'git pull ...') before pushing again.\n" +
|
||||
"hint: See the 'Note about fast-forwards' in 'git push --help' for details.";
|
||||
|
||||
@Test
|
||||
public void success() {
|
||||
String output = " \trefs/heads/master:refs/heads/master\t3e62822..a537351";
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output, SUCCESS_SUFFIX));
|
||||
assertResult(GitPushNativeResult.Type.SUCCESS, "3e62822..a537351", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejected() {
|
||||
String output = "!\trefs/heads/master:refs/heads/master\t[rejected] (non-fast-forward)";
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output, REJECT_SUFFIX));
|
||||
assertResult(GitPushNativeResult.Type.REJECTED, null, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedUpdate() {
|
||||
String output = "+\trefs/heads/master:refs/heads/master\tb9b3235...23760f8 (forced update)";
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(PUSH_DEFAULT_WARNING, TARGET_PREFIX, output));
|
||||
assertResult(GitPushNativeResult.Type.FORCED_UPDATE, "b9b3235...23760f8", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void upToDate() {
|
||||
String output = "=\trefs/heads/master:refs/heads/master\t[up to date]";
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(TARGET_PREFIX, output));
|
||||
assertResult(GitPushNativeResult.Type.UP_TO_DATE, null, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newRef() {
|
||||
String output = "*\trefs/heads/feature:refs/heads/feature2\t[new branch]";
|
||||
GitPushNativeResult result = GitPushNativeResultParser.parse(Arrays.asList(STANDARD_PREFIX, TARGET_PREFIX, output));
|
||||
assertResult(GitPushNativeResult.Type.NEW_REF, null, result);
|
||||
}
|
||||
|
||||
private static void assertResult(GitPushNativeResult.Type expectedType, String expectedRange, GitPushNativeResult actualResult) {
|
||||
assertEquals(expectedType, actualResult.getType());
|
||||
assertEquals(expectedRange, actualResult.getRange());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushSpec;
|
||||
import com.intellij.openapi.util.Trinity;
|
||||
import com.intellij.openapi.vcs.AbstractVcsHelper;
|
||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
|
||||
import com.intellij.testFramework.fixtures.TempDirTestFixture;
|
||||
import git4idea.*;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.test.GitPlatformTest;
|
||||
import git4idea.test.GitTestUtil;
|
||||
import git4idea.test.MockVcsHelper;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static com.intellij.openapi.vcs.Executor.append;
|
||||
import static git4idea.test.GitExecutor.*;
|
||||
|
||||
abstract class GitPushOperationBaseTest extends GitPlatformTest {
|
||||
|
||||
private TempDirTestFixture myOutside;
|
||||
protected String myExternalPath;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
try {
|
||||
myOutside = IdeaTestFixtureFactory.getFixtureFactory().createTempDirTestFixture();
|
||||
myOutside.setUp();
|
||||
myExternalPath = myOutside.getTempDirPath();
|
||||
}
|
||||
catch (Exception e) {
|
||||
super.tearDown();
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
GitTestUtil.overrideService(myProject, AbstractVcsHelper.class, MockVcsHelper.class); // todo temporary: to handle merge dialog
|
||||
}
|
||||
catch (Exception e) {
|
||||
tearDown();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
try {
|
||||
myOutside.tearDown();
|
||||
}
|
||||
finally {
|
||||
super.tearDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refresh() {
|
||||
super.refresh();
|
||||
myGitRepositoryManager.updateAllRepositories();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected Collection<String> getDebugLogCategories() {
|
||||
return Collections.singletonList("#" + GitPushOperation.class.getName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Trinity<GitRepository, File, File> setupRepositories(String repoRoot, String parentName, String broName) {
|
||||
File parentRepo = createParentRepo(parentName);
|
||||
File broRepo = createBroRepo(broName, parentRepo);
|
||||
|
||||
GitRepository repository = GitTestUtil.createRepository(myProject, repoRoot);
|
||||
cd(repository);
|
||||
git("remote add origin " + parentRepo.getPath());
|
||||
git("push --set-upstream origin master:master");
|
||||
|
||||
cd(broRepo.getPath());
|
||||
git("pull");
|
||||
|
||||
return Trinity.create(repository, parentRepo, broRepo);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private File createParentRepo(@NotNull String parentName) {
|
||||
cd(myExternalPath);
|
||||
git("init --bare " + parentName + ".git");
|
||||
return new File(myExternalPath, parentName + ".git");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private File createBroRepo(@NotNull String broName, @NotNull File parentRepo) {
|
||||
cd(myExternalPath);
|
||||
git("clone " + parentRepo.getName() + " " + broName);
|
||||
return new File(myExternalPath, broName);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static PushSpec<GitPushSource, GitPushTarget> makePushSpec(@NotNull GitRepository repository,
|
||||
@NotNull String from,
|
||||
@NotNull String to) {
|
||||
GitLocalBranch source = repository.getBranches().findLocalBranch(from);
|
||||
assertNotNull(source);
|
||||
GitRemoteBranch target = (GitRemoteBranch)repository.getBranches().findBranchByName(to);
|
||||
boolean newBranch;
|
||||
if (target == null) {
|
||||
int firstSlash = to.indexOf('/');
|
||||
GitRemote remote = GitUtil.findRemoteByName(repository, to.substring(0, firstSlash));
|
||||
assertNotNull(remote);
|
||||
target = new GitStandardRemoteBranch(remote, to.substring(firstSlash + 1), GitBranch.DUMMY_HASH);
|
||||
newBranch = true;
|
||||
}
|
||||
else {
|
||||
newBranch = false;
|
||||
}
|
||||
return new PushSpec<GitPushSource, GitPushTarget>(new GitPushSource(source), new GitPushTarget(target, newBranch));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static String makeCommit(String file) throws IOException {
|
||||
append(file, "some content");
|
||||
addCommit("some message");
|
||||
return last();
|
||||
}
|
||||
|
||||
protected void assertResult(@NotNull GitPushRepoResult.Type type, int pushedCommits, @NotNull String from, @NotNull String to,
|
||||
@Nullable GitUpdateResult updateResult,
|
||||
@NotNull GitPushRepoResult actualResult) {
|
||||
String message = "Result is incorrect: " + actualResult;
|
||||
assertEquals(message, type, actualResult.getType());
|
||||
assertEquals(message, pushedCommits, actualResult.getNumberOfPushedCommits());
|
||||
assertEquals(message, GitBranch.REFS_HEADS_PREFIX + from, actualResult.getSourceBranch());
|
||||
assertEquals(message, GitBranch.REFS_REMOTES_PREFIX + to, actualResult.getTargetBranch());
|
||||
assertEquals(message, updateResult, actualResult.getUpdateResult());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushSpec;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.Trinity;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.commands.Git;
|
||||
import git4idea.commands.GitCommandResult;
|
||||
import git4idea.commands.GitImpl;
|
||||
import git4idea.commands.GitLineHandlerListener;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.test.GitTestUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static git4idea.test.GitExecutor.*;
|
||||
|
||||
public class GitPushOperationMultiRepoTest extends GitPushOperationBaseTest {
|
||||
|
||||
private GitRepository myCommunity;
|
||||
private GitRepository myRepository;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
try {
|
||||
super.setUp();
|
||||
}
|
||||
catch (Exception e) {
|
||||
super.tearDown();
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
Trinity<GitRepository, File, File> mainRepo = setupRepositories(myProjectPath, "parent", "bro");
|
||||
myRepository = mainRepo.first;
|
||||
|
||||
File community = new File(myProjectPath, "community");
|
||||
assertTrue(community.mkdir());
|
||||
Trinity<GitRepository, File, File> enclosingRepo = setupRepositories(community.getPath(),
|
||||
"community_parent", "community_bro");
|
||||
myCommunity = enclosingRepo.first;
|
||||
|
||||
cd(myProjectPath);
|
||||
refresh();
|
||||
}
|
||||
catch (Exception e) {
|
||||
tearDown();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailingPushGit extends GitImpl {
|
||||
private Condition<GitRepository> myPushShouldFail;
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult push(GitRepository repository,
|
||||
GitLocalBranch source,
|
||||
GitRemoteBranch target,
|
||||
boolean force,
|
||||
boolean updateTracking,
|
||||
GitLineHandlerListener... listeners) {
|
||||
if (myPushShouldFail.value(repository)) {
|
||||
return new GitCommandResult(false, 128, Arrays.asList("Failed to push to " + target.getName()),
|
||||
Collections.<String>emptyList(), null);
|
||||
}
|
||||
return super.push(repository, source, target, force, updateTracking, listeners);
|
||||
}
|
||||
}
|
||||
|
||||
public void test_try_push_from_all_roots_even_if_one_fails() throws IOException {
|
||||
FailingPushGit failingPushGit = GitTestUtil.overrideService(Git.class, FailingPushGit.class);
|
||||
// fail in the first repo
|
||||
failingPushGit.myPushShouldFail = new Condition<GitRepository>() {
|
||||
@Override
|
||||
public boolean value(GitRepository repository) {
|
||||
return repository.equals(myRepository);
|
||||
}
|
||||
};
|
||||
|
||||
cd(myRepository);
|
||||
makeCommit("file.txt");
|
||||
cd(myCommunity);
|
||||
makeCommit("com.txt");
|
||||
|
||||
PushSpec<GitPushSource, GitPushTarget> spec1 = makePushSpec(myRepository, "master", "origin/master");
|
||||
PushSpec<GitPushSource, GitPushTarget> spec2 = makePushSpec(myCommunity, "master", "origin/master");
|
||||
Map<GitRepository, PushSpec<GitPushSource, GitPushTarget>> map = ContainerUtil.newHashMap();
|
||||
map.put(myRepository, spec1);
|
||||
map.put(myCommunity, spec2);
|
||||
GitPushResult result = new GitPushOperation(myProject, map, false).execute();
|
||||
|
||||
GitPushRepoResult result1 = result.getResults().get(myRepository);
|
||||
GitPushRepoResult result2 = result.getResults().get(myCommunity);
|
||||
|
||||
assertResult(GitPushRepoResult.Type.ERROR, -1, "master", "origin/master", null, result1);
|
||||
assertEquals("Error text is incorrect", "Failed to push to origin/master", result1.getError());
|
||||
assertResult(GitPushRepoResult.Type.SUCCESS, 1, "master", "origin/master", null, result2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.dvcs.push.PushSpec;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.openapi.util.Trinity;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.update.FileGroup;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.branch.GitBranchUtil;
|
||||
import git4idea.config.UpdateMethod;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.test.TestDialogHandler;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static git4idea.push.GitPushRepoResult.Type.*;
|
||||
import static git4idea.test.GitExecutor.*;
|
||||
|
||||
public class GitPushOperationSingleRepoTest extends GitPushOperationBaseTest {
|
||||
|
||||
protected GitRepository myRepository;
|
||||
protected File myParentRepo;
|
||||
protected File myBroRepo;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
try {
|
||||
super.setUp();
|
||||
}
|
||||
catch (Exception e) {
|
||||
super.tearDown();
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
Trinity<GitRepository, File, File> trinity = setupRepositories(myProjectPath, "parent", "bro");
|
||||
myParentRepo = trinity.second;
|
||||
myBroRepo = trinity.third;
|
||||
myRepository = trinity.first;
|
||||
|
||||
cd(myProjectPath);
|
||||
refresh();
|
||||
}
|
||||
catch (Exception e) {
|
||||
tearDown();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void test_successful_push() throws IOException {
|
||||
String hash = makeCommit("file.txt");
|
||||
GitPushResult result = push("master", "origin/master");
|
||||
|
||||
assertResult(SUCCESS, 1, "master", "origin/master", result);
|
||||
assertPushed(hash, "master");
|
||||
}
|
||||
|
||||
public void test_push_new_branch() {
|
||||
git("checkout -b feature");
|
||||
GitPushResult result = push("feature", "origin/feature");
|
||||
|
||||
assertResult(NEW_BRANCH, -1, "feature", "origin/feature", result);
|
||||
assertBranchExists("feature");
|
||||
}
|
||||
|
||||
public void test_push_new_branch_with_commits() {
|
||||
touch("feature.txt", "content");
|
||||
addCommit("feature commit");
|
||||
String hash = last();
|
||||
git("checkout -b feature");
|
||||
GitPushResult result = push("feature", "origin/feature");
|
||||
|
||||
assertResult(NEW_BRANCH, -1, "feature", "origin/feature", result);
|
||||
assertBranchExists("feature");
|
||||
assertPushed(hash, "feature");
|
||||
}
|
||||
|
||||
public void test_upstream_is_set_for_new_branch() {
|
||||
git("checkout -b feature");
|
||||
push("feature", "origin/feature");
|
||||
assertUpstream("feature", "origin", "feature");
|
||||
}
|
||||
|
||||
public void test_upstream_is_not_modified_if_already_set() {
|
||||
push("master", "origin/feature");
|
||||
assertUpstream("master", "origin", "master");
|
||||
}
|
||||
|
||||
public void test_rejected_push_to_tracked_branch_proposes_to_update() throws IOException {
|
||||
pushCommitFromBro();
|
||||
|
||||
final Ref<Boolean> dialogShown = Ref.create(false);
|
||||
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
|
||||
@Override
|
||||
public int handleDialog(DialogWrapper dialog) {
|
||||
dialogShown.set(true);
|
||||
return DialogWrapper.CANCEL_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
GitPushResult result = push("master", "origin/master");
|
||||
|
||||
assertTrue("Rejected push dialog wasn't shown", dialogShown.get());
|
||||
assertResult(REJECTED, -1, "master", "origin/master", result);
|
||||
}
|
||||
|
||||
public void test_rejected_push_to_other_branch_doesnt_propose_to_update() throws IOException {
|
||||
pushCommitFromBro();
|
||||
cd(myRepository);
|
||||
git("checkout -b feature");
|
||||
|
||||
final Ref<Boolean> dialogShown = Ref.create(false);
|
||||
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
|
||||
@Override
|
||||
public int handleDialog(DialogWrapper dialog) {
|
||||
dialogShown.set(true);
|
||||
return DialogWrapper.CANCEL_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
GitPushResult result = push("feature", "origin/master");
|
||||
|
||||
assertFalse("Rejected push dialog shouldn't be shown", dialogShown.get());
|
||||
assertResult(REJECTED, -1, "feature", "origin/master", result);
|
||||
}
|
||||
|
||||
public void test_push_is_rejected_too_many_times() throws IOException {
|
||||
pushCommitFromBro();
|
||||
cd(myRepository);
|
||||
String hash = makeCommit("afile.txt");
|
||||
|
||||
agreeToUpdate(GitRejectedPushUpdateDialog.MERGE_EXIT_CODE);
|
||||
|
||||
refresh();
|
||||
PushSpec<GitPushSource, GitPushTarget> pushSpec = makePushSpec(myRepository, "master", "origin/master");
|
||||
|
||||
GitPushResult result = new GitPushOperation(myProject, Collections.singletonMap(myRepository, pushSpec), false) {
|
||||
@NotNull
|
||||
@Override
|
||||
protected GitUpdateResult update(@NotNull Collection<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
|
||||
GitUpdateResult updateResult = super.update(rootsToUpdate, updateMethod);
|
||||
try {
|
||||
pushCommitFromBro();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return updateResult;
|
||||
}
|
||||
}.execute();
|
||||
assertResult(REJECTED, -1, "master", "origin/master", GitUpdateResult.SUCCESS, Arrays.asList("bro.txt"), result);
|
||||
|
||||
cd(myParentRepo.getPath());
|
||||
String history = git("log --all --pretty=%H ");
|
||||
assertFalse("The commit shouldn't be pushed", history.contains(hash));
|
||||
}
|
||||
|
||||
public void test_force_push() throws IOException {
|
||||
String lostHash = pushCommitFromBro();
|
||||
cd(myRepository);
|
||||
String hash = makeCommit("anyfile.txt");
|
||||
|
||||
GitPushResult result = push("master", "origin/master", true);
|
||||
|
||||
assertResult(FORCED, -1, "master", "origin/master", result);
|
||||
|
||||
cd(myParentRepo.getPath());
|
||||
String history = git("log --all --pretty=%H ");
|
||||
assertFalse(history.contains(lostHash));
|
||||
assertEquals(hash, StringUtil.splitByLines(history)[0]);
|
||||
}
|
||||
|
||||
public void test_merge_after_rejected_push() throws IOException {
|
||||
String broHash = pushCommitFromBro();
|
||||
cd(myRepository);
|
||||
String hash = makeCommit("file.txt");
|
||||
|
||||
agreeToUpdate(GitRejectedPushUpdateDialog.MERGE_EXIT_CODE);
|
||||
|
||||
GitPushResult result = push("master", "origin/master");
|
||||
|
||||
cd(myRepository);
|
||||
String log = git("log -3 --pretty=%H#%s");
|
||||
String[] commits = StringUtil.splitByLines(log);
|
||||
String lastCommitMsg = commits[0].split("#")[1];
|
||||
assertTrue("The last commit doesn't look like a merge commit: " + lastCommitMsg, lastCommitMsg.contains("Merge"));
|
||||
assertEquals(hash, commits[1].split("#")[0]);
|
||||
assertEquals(broHash, commits[2].split("#")[0]);
|
||||
|
||||
assertResult(SUCCESS, 2, "master", "origin/master", GitUpdateResult.SUCCESS, Arrays.asList("bro.txt"), result);
|
||||
}
|
||||
|
||||
public void test_update_with_conflicts_cancels_push() throws IOException {
|
||||
cd(myBroRepo.getPath());
|
||||
append("bro.txt", "bro content");
|
||||
makeCommit("msg");
|
||||
git("push origin master:master");
|
||||
|
||||
cd(myRepository);
|
||||
append("bro.txt", "main content");
|
||||
makeCommit("msg");
|
||||
|
||||
agreeToUpdate(GitRejectedPushUpdateDialog.REBASE_EXIT_CODE);
|
||||
|
||||
GitPushResult result = push("master", "origin/master");
|
||||
assertResult(REJECTED, -1, "master", "origin/master", GitUpdateResult.INCOMPLETE, Arrays.asList("bro.txt"), result);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitPushResult push(@NotNull String from, @NotNull String to) {
|
||||
return push(from, to, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitPushResult push(@NotNull String from, @NotNull String to, boolean force) {
|
||||
refresh();
|
||||
PushSpec<GitPushSource, GitPushTarget> spec = makePushSpec(myRepository, from, to);
|
||||
return new GitPushOperation(myProject, Collections.singletonMap(myRepository, spec), force).execute();
|
||||
}
|
||||
|
||||
private void agreeToUpdate(final int exitCode) {
|
||||
myDialogManager.registerDialogHandler(GitRejectedPushUpdateDialog.class, new TestDialogHandler() {
|
||||
@Override
|
||||
public int handleDialog(DialogWrapper dialog) {
|
||||
return exitCode;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String pushCommitFromBro() throws IOException {
|
||||
cd(myBroRepo.getPath());
|
||||
String hash = makeCommit("bro.txt");
|
||||
git("push");
|
||||
return hash;
|
||||
}
|
||||
|
||||
private void assertResult(GitPushRepoResult.Type type, int pushedCommits, String from, String to, GitPushResult actualResult) {
|
||||
assertResult(type, pushedCommits, from, to, null, null, actualResult);
|
||||
}
|
||||
|
||||
private void assertResult(@NotNull GitPushRepoResult.Type type, int pushedCommits, @NotNull String from, @NotNull String to,
|
||||
@Nullable GitUpdateResult updateResult,
|
||||
@Nullable List<String> updatedFiles,
|
||||
@NotNull GitPushResult actualResult) {
|
||||
assertResult(type, pushedCommits, from, to, updateResult, actualResult.getResults().get(myRepository));
|
||||
assertSameElements("Updated files set is incorrect",
|
||||
getUpdatedFiles(actualResult.getUpdatedFiles()), ContainerUtil.notNullize(updatedFiles));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Collection<String> getUpdatedFiles(@NotNull UpdatedFiles updatedFiles) {
|
||||
Collection<String> result = ContainerUtil.newArrayList();
|
||||
for (FileGroup group : updatedFiles.getTopLevelGroups()) {
|
||||
result.addAll(getUpdatedFiles(group));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Collection<String> getUpdatedFiles(@NotNull FileGroup group) {
|
||||
Function<String, String> getRelative = new Function<String, String>() {
|
||||
@Override
|
||||
public String fun(String path) {
|
||||
return FileUtil.getRelativePath(new File(myProjectPath), new File(path));
|
||||
}
|
||||
};
|
||||
Collection<String> result = ContainerUtil.newArrayList();
|
||||
result.addAll(ContainerUtil.map(group.getFiles(), getRelative));
|
||||
for (FileGroup child : group.getChildren()) {
|
||||
result.addAll(getUpdatedFiles(child));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void assertPushed(String expectedHash, String branch) {
|
||||
cd(myParentRepo.getPath());
|
||||
String actualHash = git("log -1 --pretty=%H " + branch);
|
||||
assertEquals(expectedHash, actualHash);
|
||||
}
|
||||
|
||||
private void assertBranchExists(String branch) {
|
||||
cd(myParentRepo.getPath());
|
||||
String out = git("branch");
|
||||
assertTrue(out.contains(branch));
|
||||
}
|
||||
|
||||
private static void assertUpstream(final String localBranch,
|
||||
String expectedUpstreamRemote,
|
||||
String expectedUpstreamBranch) {
|
||||
String upstreamRemote = GitBranchUtil.stripRefsPrefix(git("config branch." + localBranch + ".remote"));
|
||||
String upstreamBranch = GitBranchUtil.stripRefsPrefix(git("config branch." + localBranch + ".merge"));
|
||||
assertEquals(expectedUpstreamRemote, upstreamRemote);
|
||||
assertEquals(expectedUpstreamBranch, upstreamBranch);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2000-2014 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.push;
|
||||
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationType;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Computable;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.vcs.update.UpdatedFiles;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitLocalBranch;
|
||||
import git4idea.GitRemoteBranch;
|
||||
import git4idea.GitStandardRemoteBranch;
|
||||
import git4idea.repo.GitRemote;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.test.GitPlatformTest;
|
||||
import git4idea.test.MockGitRepository;
|
||||
import git4idea.update.GitUpdateResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static git4idea.push.GitPushNativeResult.Type.*;
|
||||
|
||||
public class GitPushResultNotificationTest extends GitPlatformTest {
|
||||
|
||||
private static Project ourProject;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
ourProject = myProject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
ourProject = null;
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void test_single_success() {
|
||||
GitPushResultNotification notification = notification(singleResult(SUCCESS, "master", "origin/master", 1));
|
||||
assertNotification(NotificationType.INFORMATION, "Push successful", "Pushed 1 commit to origin/master", notification);
|
||||
}
|
||||
|
||||
public void test_pushed_new_branch() {
|
||||
GitPushResultNotification notification = notification(singleResult(NEW_REF, "feature", "origin/feature", -1));
|
||||
assertNotification(NotificationType.INFORMATION, "Push successful", "Pushed feature to new branch origin/feature", notification);
|
||||
}
|
||||
|
||||
public void test_force_pushed() {
|
||||
GitPushResultNotification notification = notification(singleResult(FORCED_UPDATE, "feature", "origin/feature", -1));
|
||||
assertNotification(NotificationType.INFORMATION, "Push successful", "Force pushed feature to origin/feature", notification);
|
||||
}
|
||||
|
||||
public void test_success_and_fail() {
|
||||
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
|
||||
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
|
||||
put(repo("community"), repoResult(ERROR, "master", "origin/master", "Permission denied"));
|
||||
}});
|
||||
assertNotification(NotificationType.ERROR, "Push partially failed",
|
||||
"ultimate: pushed 1 commit to origin/master<br/>" +
|
||||
"community: failed with error: Permission denied", notification);
|
||||
}
|
||||
|
||||
public void test_success_and_reject() {
|
||||
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
|
||||
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
|
||||
put(repo("community"), repoResult(REJECTED, "master", "origin/master", -1));
|
||||
}});
|
||||
assertNotification(NotificationType.WARNING, "Push partially rejected",
|
||||
"ultimate: pushed 1 commit to origin/master<br/>" +
|
||||
"community: push to origin/master was rejected", notification);
|
||||
}
|
||||
|
||||
public void test_success_with_update() {
|
||||
GitPushResultNotification notification = notification(singleResult(SUCCESS, "master", "origin/master", 2, GitUpdateResult.SUCCESS));
|
||||
assertNotification(NotificationType.INFORMATION, "Push successful",
|
||||
"Pushed 2 commits to origin/master<br/>" +
|
||||
GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification);
|
||||
|
||||
}
|
||||
|
||||
public void test_success_and_resolved_conflicts() {
|
||||
GitPushResultNotification notification = notification(new HashMap<GitRepository, GitPushRepoResult>() {{
|
||||
put(repo("community"), repoResult(REJECTED, "master", "origin/master", -1, GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS));
|
||||
put(repo("contrib"), repoResult(REJECTED, "master", "origin/master", -1, GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS));
|
||||
put(repo("ultimate"), repoResult(SUCCESS, "master", "origin/master", 1));
|
||||
}});
|
||||
assertNotification(NotificationType.WARNING, "Push partially rejected",
|
||||
"ultimate: pushed 1 commit to origin/master<br/>" +
|
||||
"community: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "<br/>" +
|
||||
"contrib: " + GitPushResultNotification.UPDATE_WITH_RESOLVED_CONFLICTS + "<br/>" +
|
||||
GitPushResultNotification.VIEW_FILES_UPDATED_DURING_THE_PUSH, notification);
|
||||
|
||||
}
|
||||
|
||||
private static Map<GitRepository, GitPushRepoResult> singleResult(final GitPushNativeResult.Type type,
|
||||
final String from,
|
||||
final String to,
|
||||
final int commits,
|
||||
@Nullable final GitUpdateResult updateResult) {
|
||||
return new HashMap<GitRepository, GitPushRepoResult>() {{
|
||||
put(repo("community"), repoResult(type, from, to, commits, updateResult));
|
||||
}};
|
||||
}
|
||||
|
||||
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, int commits) {
|
||||
return repoResult(nativeType, from, to, commits, null);
|
||||
}
|
||||
|
||||
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, int commits,
|
||||
@Nullable GitUpdateResult updateResult) {
|
||||
GitPushNativeResult nr = new GitPushNativeResult(nativeType, "");
|
||||
return GitPushRepoResult.addUpdateResult(GitPushRepoResult.convertFromNative(nr, commits, makeLocalBranch(from), makeRemoteBranch(to)),
|
||||
updateResult);
|
||||
}
|
||||
|
||||
private static Map<GitRepository, GitPushRepoResult> singleResult(final GitPushNativeResult.Type type,
|
||||
final String from,
|
||||
final String to, final int commits) {
|
||||
return singleResult(type, from, to, commits, null);
|
||||
}
|
||||
|
||||
// keep params for unification
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
private static GitPushRepoResult repoResult(GitPushNativeResult.Type nativeType, String from, String to, String errorText) {
|
||||
GitPushNativeResult nr1 = GitPushNativeResult.error(errorText);
|
||||
return GitPushRepoResult.convertFromNative(nr1, -1, makeLocalBranch(from), makeRemoteBranch(to));
|
||||
}
|
||||
|
||||
private static GitLocalBranch makeLocalBranch(String from) {
|
||||
return new GitLocalBranch(from, GitBranch.DUMMY_HASH);
|
||||
}
|
||||
|
||||
private static GitRemoteBranch makeRemoteBranch(String to) {
|
||||
int firstSlash = to.indexOf('/');
|
||||
GitRemote remote = new GitRemote(to.substring(0, firstSlash), Collections.<String>emptyList(), Collections.<String>emptyList(),
|
||||
Collections.<String>emptyList(), Collections.<String>emptyList());
|
||||
return new GitStandardRemoteBranch(remote, to.substring(firstSlash + 1), GitBranch.DUMMY_HASH);
|
||||
}
|
||||
|
||||
private GitPushResultNotification notification(Map<GitRepository, GitPushRepoResult> map) {
|
||||
boolean wasUpdatePerformed = ContainerUtil.exists(map.values(), new Condition<GitPushRepoResult>() {
|
||||
@Override
|
||||
public boolean value(GitPushRepoResult aNew) {
|
||||
return aNew.getUpdateResult() != null;
|
||||
}
|
||||
});
|
||||
UpdatedFiles updatedFiles = UpdatedFiles.create();
|
||||
if (wasUpdatePerformed) {
|
||||
updatedFiles.getTopLevelGroups().get(0).add("file.txt", "Git", null);
|
||||
}
|
||||
return GitPushResultNotification.create(myProject, new GitPushResult(map, updatedFiles, null, null), map.size() > 1);
|
||||
}
|
||||
|
||||
private static MockGitRepository repo(final String name) {
|
||||
VirtualFile root = ApplicationManager.getApplication().runWriteAction(new Computable<VirtualFile>() {
|
||||
@Override
|
||||
public VirtualFile compute() {
|
||||
try {
|
||||
return ourProject.getBaseDir().createChildData(null, name);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
return new MockGitRepository(ourProject, root);
|
||||
}
|
||||
|
||||
private static void assertNotification(NotificationType type, String title, String content, Notification actual) {
|
||||
assertEquals(type, actual.getType());
|
||||
assertEquals(title, actual.getTitle());
|
||||
assertEquals(content, actual.getContent());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user