IDEA-64049 Git: before commit check if user.name and user.email have been set, otherw...
[idea/community.git] / plugins / git4idea / src / git4idea / checkin / GitCheckinHandlerFactory.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package git4idea.checkin;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.ui.Messages;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.vcs.CheckinProjectPanel;
24 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
25 import com.intellij.openapi.vcs.VcsException;
26 import com.intellij.openapi.vcs.changes.CommitExecutor;
27 import com.intellij.openapi.vcs.checkin.CheckinHandler;
28 import com.intellij.openapi.vcs.checkin.VcsCheckinHandlerFactory;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.util.PairConsumer;
31 import git4idea.GitVcs;
32 import git4idea.config.GitConfigUtil;
33 import git4idea.i18n.GitBundle;
34 import git4idea.repo.GitRepository;
35 import git4idea.repo.GitRepositoryManager;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.util.*;
40
41 /**
42  * Prohibits commiting with an empty messages.
43  * @author Kirill Likhodedov
44 */
45 public class GitCheckinHandlerFactory extends VcsCheckinHandlerFactory {
46   
47   private static final Logger LOG = Logger.getInstance(GitCheckinHandlerFactory.class);
48
49   public GitCheckinHandlerFactory() {
50     super(GitVcs.getKey());
51   }
52
53   @NotNull
54   @Override
55   protected CheckinHandler createVcsHandler(final CheckinProjectPanel panel) {
56     return new MyCheckinHandler(panel);
57   }
58
59   private class MyCheckinHandler extends CheckinHandler {
60     private CheckinProjectPanel myPanel;
61
62     public MyCheckinHandler(CheckinProjectPanel panel) {
63       myPanel = panel;
64     }
65
66     @Override
67     public ReturnResult beforeCheckin(@Nullable CommitExecutor executor, PairConsumer<Object, Object> additionalDataConsumer) {
68       if (emptyCommitMessage()) {
69         return ReturnResult.CANCEL;
70       }
71       
72       ReturnResult result = checkUserName();
73       if (result != ReturnResult.COMMIT) {
74         return result;
75       }
76
77       if (commitOrCommitAndPush(executor)) {
78         return warnAboutDetachedHeadIfNeeded();
79       }
80       return ReturnResult.COMMIT;
81     }
82
83     private ReturnResult checkUserName() {
84       Project project = myPanel.getProject();
85       Collection<VirtualFile> notDefined = new ArrayList<VirtualFile>();
86       Map<VirtualFile, Pair<String, String>> defined = new HashMap<VirtualFile, Pair<String, String>>();
87       Collection<VirtualFile> allRoots = new ArrayList<VirtualFile>(Arrays.asList(
88         ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(GitVcs.getInstance(project))));
89
90       Collection<VirtualFile> affectedRoots = myPanel.getRoots();
91       for (VirtualFile root : affectedRoots) {
92         try {
93           Pair<String, String> nameAndEmail = getUserNameAndEmailFromGitConfig(project, root);
94           String name = nameAndEmail.getFirst();
95           String email = nameAndEmail.getSecond();
96           if (name == null || email == null) {
97             notDefined.add(root);
98           }
99           else {
100             defined.put(root, nameAndEmail);
101           }
102         }
103         catch (VcsException e) {
104           LOG.error("Couldn't get user.name and user.email for root " + root, e);
105           // doing nothing - let commit with possibly empty user.name/email
106         }
107       }
108       
109       if (notDefined.isEmpty()) {
110         return ReturnResult.COMMIT;
111       }
112
113       if (defined.isEmpty() && allRoots.size() > affectedRoots.size()) {
114         allRoots.removeAll(affectedRoots);
115         for (VirtualFile root : allRoots) {
116           try {
117             Pair<String, String> nameAndEmail = getUserNameAndEmailFromGitConfig(project, root);
118             String name = nameAndEmail.getFirst();
119             String email = nameAndEmail.getSecond();
120             if (name != null && email != null) {
121               defined.put(root, nameAndEmail);
122               break;
123             }
124           }
125           catch (VcsException e) {
126             LOG.error("Couldn't get user.name and user.email for root " + root, e);
127             // doing nothing - not critical not to find the values for other roots not affected by commit
128           }
129         }
130       }
131
132       GitUserNameNotDefinedDialog dialog = new GitUserNameNotDefinedDialog(project, notDefined, affectedRoots, defined);
133       dialog.show();
134       if (dialog.isOK()) {
135         try {
136           if (dialog.isGlobal()) {
137             GitConfigUtil.setValue(project, notDefined.iterator().next(), GitConfigUtil.USER_NAME, dialog.getUserName(), "--global");
138             GitConfigUtil.setValue(project, notDefined.iterator().next(), GitConfigUtil.USER_EMAIL, dialog.getUserEmail(), "--global");
139           }
140           else {
141             for (VirtualFile root : notDefined) {
142               GitConfigUtil.setValue(project, root, GitConfigUtil.USER_NAME, dialog.getUserName());
143               GitConfigUtil.setValue(project, root, GitConfigUtil.USER_EMAIL, dialog.getUserEmail());
144             }
145           }
146         }
147         catch (VcsException e) {
148           String message = "Couldn't set user.name and user.email";
149           LOG.error(message, e);
150           Messages.showErrorDialog(myPanel.getComponent(), message);
151           return ReturnResult.CANCEL;
152         }
153         return ReturnResult.COMMIT;
154       }
155       return ReturnResult.CLOSE_WINDOW;
156     }
157     
158     @NotNull
159     private Pair<String, String> getUserNameAndEmailFromGitConfig(@NotNull Project project, @NotNull VirtualFile root) throws VcsException {
160       String name = GitConfigUtil.getValue(project, root, GitConfigUtil.USER_NAME);
161       String email = GitConfigUtil.getValue(project, root, GitConfigUtil.USER_EMAIL);
162       return Pair.create(name, email);
163     }
164     
165     private boolean emptyCommitMessage() {
166       if (myPanel.getCommitMessage().trim().isEmpty()) {
167         Messages.showMessageDialog(myPanel.getComponent(), GitBundle.message("git.commit.message.empty"),
168                                    GitBundle.message("git.commit.message.empty.title"), Messages.getErrorIcon());
169         return true;
170       }
171       return false;
172     }
173
174     private ReturnResult warnAboutDetachedHeadIfNeeded() {
175       // Warning: commit on a detached HEAD
176       DetachedRoot detachedRoot = getDetachedRoot();
177       if (detachedRoot == null) {
178         return ReturnResult.COMMIT;
179       }
180
181       final String title;
182       final String message;
183       final CharSequence rootPath = StringUtil.last(detachedRoot.myRoot.getPresentableUrl(), 50, true);
184       final String messageCommonStart = "The Git repository <code>" + rootPath + "</code>";
185       if (detachedRoot.myRebase) {
186         title = "Unfinished rebase process";
187         message = messageCommonStart + " <br/> has an <b>unfinished rebase</b> process. <br/>" +
188                   "You probably want to <b>continue rebase</b> instead of committing. <br/>" +
189                   "Committing during rebase may lead to the commit loss. <br/>" +
190                   readMore("http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html", "Read more about Git rebase");
191       } else {
192         title = "Commit in detached HEAD may be dangerous";
193         message = messageCommonStart + " is in the <b>detached HEAD</b> state. <br/>" +
194                   "You can look around, make experimental changes and commit them, but be sure to checkout a branch not to lose your work. <br/>" +
195                   "Otherwise you risk losing your changes. <br/>" +
196                   readMore("http://sitaramc.github.com/concepts/detached-head.html", "Read more about detached HEAD");
197       }
198
199       final int choice = Messages.showOkCancelDialog(myPanel.getComponent(), "<html>" + message + "</html>", title,
200                                                      "Cancel", "Commit", Messages.getWarningIcon());
201       if (choice == 1) {
202         return ReturnResult.COMMIT;
203       } else {
204         return ReturnResult.CLOSE_WINDOW;
205       }
206     }
207
208     private boolean commitOrCommitAndPush(@Nullable CommitExecutor executor) {
209       return executor == null || executor instanceof GitCommitAndPushExecutor;
210     }
211
212     private String readMore(String link, String message) {
213       if (Messages.canShowMacSheetPanel()) {
214         return message + ":\n" + link;
215       }
216       else {
217         return String.format("<a href='%s'>%s</a>.", link, message);
218       }
219     }
220
221     /**
222      * Scans the Git roots, selected for commit, for the root which is on a detached HEAD.
223      * Returns null, if all repositories are on the branch.
224      * There might be several detached repositories, - in that case only one is returned.
225      * This is because the situation is very rare, while it requires a lot of additional effort of making a well-formed message.
226      */
227     @Nullable
228     private DetachedRoot getDetachedRoot() {
229       GitRepositoryManager repositoryManager = GitRepositoryManager.getInstance(myPanel.getProject());
230       for (VirtualFile root : myPanel.getRoots()) {
231         GitRepository repository = repositoryManager.getRepositoryForRoot(root);
232         if (repository == null) {
233           continue;
234         }
235         if (!repository.isOnBranch()) {
236           return new DetachedRoot(root, repository.isRebaseInProgress());
237         }
238       }
239       return null;
240     }
241
242     private class DetachedRoot {
243       final VirtualFile myRoot;
244       final boolean myRebase; // rebase in progress, or just detached due to a checkout of a commit.
245
246       public DetachedRoot(@NotNull VirtualFile root, boolean rebase) {
247         myRoot = root;
248         myRebase = rebase;
249       }
250     }
251
252   }
253
254 }