Merge remote-tracking branch 'origin/master'
[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.config.GitVersion;
34 import git4idea.config.GitVersionSpecialty;
35 import git4idea.i18n.GitBundle;
36 import git4idea.repo.GitRepository;
37 import git4idea.repo.GitRepositoryManager;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.*;
42
43 /**
44  * Prohibits commiting with an empty messages.
45  * @author Kirill Likhodedov
46 */
47 public class GitCheckinHandlerFactory extends VcsCheckinHandlerFactory {
48   
49   private static final Logger LOG = Logger.getInstance(GitCheckinHandlerFactory.class);
50
51   public GitCheckinHandlerFactory() {
52     super(GitVcs.getKey());
53   }
54
55   @NotNull
56   @Override
57   protected CheckinHandler createVcsHandler(final CheckinProjectPanel panel) {
58     return new MyCheckinHandler(panel);
59   }
60
61   private class MyCheckinHandler extends CheckinHandler {
62     private CheckinProjectPanel myPanel;
63
64     public MyCheckinHandler(CheckinProjectPanel panel) {
65       myPanel = panel;
66     }
67
68     @Override
69     public ReturnResult beforeCheckin(@Nullable CommitExecutor executor, PairConsumer<Object, Object> additionalDataConsumer) {
70       if (emptyCommitMessage()) {
71         return ReturnResult.CANCEL;
72       }
73       
74       ReturnResult result = checkUserName();
75       if (result != ReturnResult.COMMIT) {
76         return result;
77       }
78
79       if (commitOrCommitAndPush(executor)) {
80         return warnAboutDetachedHeadIfNeeded();
81       }
82       return ReturnResult.COMMIT;
83     }
84
85     private ReturnResult checkUserName() {
86       Project project = myPanel.getProject();
87       GitVcs vcs = GitVcs.getInstance(project);
88       assert vcs != null;
89
90       Collection<VirtualFile> notDefined = new ArrayList<VirtualFile>();
91       Map<VirtualFile, Pair<String, String>> defined = new HashMap<VirtualFile, Pair<String, String>>();
92       Collection<VirtualFile> allRoots = new ArrayList<VirtualFile>(Arrays.asList(
93         ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(vcs)));
94
95       Collection<VirtualFile> affectedRoots = myPanel.getRoots();
96       for (VirtualFile root : affectedRoots) {
97         try {
98           Pair<String, String> nameAndEmail = getUserNameAndEmailFromGitConfig(project, root);
99           String name = nameAndEmail.getFirst();
100           String email = nameAndEmail.getSecond();
101           if (name == null || email == null) {
102             notDefined.add(root);
103           }
104           else {
105             defined.put(root, nameAndEmail);
106           }
107         }
108         catch (VcsException e) {
109           LOG.error("Couldn't get user.name and user.email for root " + root, e);
110           // doing nothing - let commit with possibly empty user.name/email
111         }
112       }
113       
114       if (notDefined.isEmpty()) {
115         return ReturnResult.COMMIT;
116       }
117
118       GitVersion version = vcs.getVersion();
119       if (System.getenv("HOME") == null && GitVersionSpecialty.DOESNT_DEFINE_HOME_ENV_VAR.existsIn(version)) {
120         Messages.showErrorDialog(project,
121                                  "You are using Git " + version + " which doesn't define %HOME% environment variable properly.\n" +
122                                  "Consider updating Git to a newer version " +
123                                  "or define %HOME% to point to the place where the global .gitconfig is stored \n" +
124                                  "(it is usually %USERPROFILE% or %HOMEDRIVE%%HOMEPATH%).",
125                                  "HOME Variable Is Not Defined");
126         return ReturnResult.CANCEL;
127       }
128
129       if (defined.isEmpty() && allRoots.size() > affectedRoots.size()) {
130         allRoots.removeAll(affectedRoots);
131         for (VirtualFile root : allRoots) {
132           try {
133             Pair<String, String> nameAndEmail = getUserNameAndEmailFromGitConfig(project, root);
134             String name = nameAndEmail.getFirst();
135             String email = nameAndEmail.getSecond();
136             if (name != null && email != null) {
137               defined.put(root, nameAndEmail);
138               break;
139             }
140           }
141           catch (VcsException e) {
142             LOG.error("Couldn't get user.name and user.email for root " + root, e);
143             // doing nothing - not critical not to find the values for other roots not affected by commit
144           }
145         }
146       }
147
148       GitUserNameNotDefinedDialog dialog = new GitUserNameNotDefinedDialog(project, notDefined, affectedRoots, defined);
149       dialog.show();
150       if (dialog.isOK()) {
151         try {
152           if (dialog.isGlobal()) {
153             GitConfigUtil.setValue(project, notDefined.iterator().next(), GitConfigUtil.USER_NAME, dialog.getUserName(), "--global");
154             GitConfigUtil.setValue(project, notDefined.iterator().next(), GitConfigUtil.USER_EMAIL, dialog.getUserEmail(), "--global");
155           }
156           else {
157             for (VirtualFile root : notDefined) {
158               GitConfigUtil.setValue(project, root, GitConfigUtil.USER_NAME, dialog.getUserName());
159               GitConfigUtil.setValue(project, root, GitConfigUtil.USER_EMAIL, dialog.getUserEmail());
160             }
161           }
162         }
163         catch (VcsException e) {
164           String message = "Couldn't set user.name and user.email";
165           LOG.error(message, e);
166           Messages.showErrorDialog(myPanel.getComponent(), message);
167           return ReturnResult.CANCEL;
168         }
169         return ReturnResult.COMMIT;
170       }
171       return ReturnResult.CLOSE_WINDOW;
172     }
173     
174     @NotNull
175     private Pair<String, String> getUserNameAndEmailFromGitConfig(@NotNull Project project, @NotNull VirtualFile root) throws VcsException {
176       String name = GitConfigUtil.getValue(project, root, GitConfigUtil.USER_NAME);
177       String email = GitConfigUtil.getValue(project, root, GitConfigUtil.USER_EMAIL);
178       return Pair.create(name, email);
179     }
180     
181     private boolean emptyCommitMessage() {
182       if (myPanel.getCommitMessage().trim().isEmpty()) {
183         Messages.showMessageDialog(myPanel.getComponent(), GitBundle.message("git.commit.message.empty"),
184                                    GitBundle.message("git.commit.message.empty.title"), Messages.getErrorIcon());
185         return true;
186       }
187       return false;
188     }
189
190     private ReturnResult warnAboutDetachedHeadIfNeeded() {
191       // Warning: commit on a detached HEAD
192       DetachedRoot detachedRoot = getDetachedRoot();
193       if (detachedRoot == null) {
194         return ReturnResult.COMMIT;
195       }
196
197       final String title;
198       final String message;
199       final CharSequence rootPath = StringUtil.last(detachedRoot.myRoot.getPresentableUrl(), 50, true);
200       final String messageCommonStart = "The Git repository <code>" + rootPath + "</code>";
201       if (detachedRoot.myRebase) {
202         title = "Unfinished rebase process";
203         message = messageCommonStart + " <br/> has an <b>unfinished rebase</b> process. <br/>" +
204                   "You probably want to <b>continue rebase</b> instead of committing. <br/>" +
205                   "Committing during rebase may lead to the commit loss. <br/>" +
206                   readMore("http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html", "Read more about Git rebase");
207       } else {
208         title = "Commit in detached HEAD may be dangerous";
209         message = messageCommonStart + " is in the <b>detached HEAD</b> state. <br/>" +
210                   "You can look around, make experimental changes and commit them, but be sure to checkout a branch not to lose your work. <br/>" +
211                   "Otherwise you risk losing your changes. <br/>" +
212                   readMore("http://sitaramc.github.com/concepts/detached-head.html", "Read more about detached HEAD");
213       }
214
215       final int choice = Messages.showOkCancelDialog(myPanel.getComponent(), "<html>" + message + "</html>", title,
216                                                      "Cancel", "Commit", Messages.getWarningIcon());
217       if (choice == 1) {
218         return ReturnResult.COMMIT;
219       } else {
220         return ReturnResult.CLOSE_WINDOW;
221       }
222     }
223
224     private boolean commitOrCommitAndPush(@Nullable CommitExecutor executor) {
225       return executor == null || executor instanceof GitCommitAndPushExecutor;
226     }
227
228     private String readMore(String link, String message) {
229       if (Messages.canShowMacSheetPanel()) {
230         return message + ":\n" + link;
231       }
232       else {
233         return String.format("<a href='%s'>%s</a>.", link, message);
234       }
235     }
236
237     /**
238      * Scans the Git roots, selected for commit, for the root which is on a detached HEAD.
239      * Returns null, if all repositories are on the branch.
240      * There might be several detached repositories, - in that case only one is returned.
241      * This is because the situation is very rare, while it requires a lot of additional effort of making a well-formed message.
242      */
243     @Nullable
244     private DetachedRoot getDetachedRoot() {
245       GitRepositoryManager repositoryManager = GitRepositoryManager.getInstance(myPanel.getProject());
246       for (VirtualFile root : myPanel.getRoots()) {
247         GitRepository repository = repositoryManager.getRepositoryForRoot(root);
248         if (repository == null) {
249           continue;
250         }
251         if (!repository.isOnBranch()) {
252           return new DetachedRoot(root, repository.isRebaseInProgress());
253         }
254       }
255       return null;
256     }
257
258     private class DetachedRoot {
259       final VirtualFile myRoot;
260       final boolean myRebase; // rebase in progress, or just detached due to a checkout of a commit.
261
262       public DetachedRoot(@NotNull VirtualFile root, boolean rebase) {
263         myRoot = root;
264         myRebase = rebase;
265       }
266     }
267
268   }
269
270 }