Check for unsaved changes before Commit
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / actions / CommitAction.java
blobb130c5bea5ba269ca2a49fedba93dac2ab972a04
1 /*******************************************************************************
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2007, Jing Xue <jingxue@digizenstudio.com>
4 * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com>
5 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
6 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
7 * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
9 * All rights reserved. This program and the accompanying materials
10 * are made available under the terms of the Eclipse Public License v1.0
11 * which accompanies this distribution, and is available at
12 * http://www.eclipse.org/legal/epl-v10.html
13 *******************************************************************************/
14 package org.eclipse.egit.ui.internal.actions;
16 import java.io.IOException;
17 import java.io.UnsupportedEncodingException;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
25 import org.eclipse.core.resources.IFile;
26 import org.eclipse.core.resources.IProject;
27 import org.eclipse.core.resources.IResource;
28 import org.eclipse.core.resources.IResourceVisitor;
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.IProgressMonitor;
31 import org.eclipse.core.runtime.IStatus;
32 import org.eclipse.core.runtime.Status;
33 import org.eclipse.core.runtime.jobs.Job;
34 import org.eclipse.egit.core.op.CommitOperation;
35 import org.eclipse.egit.core.project.GitProjectData;
36 import org.eclipse.egit.core.project.RepositoryMapping;
37 import org.eclipse.egit.ui.Activator;
38 import org.eclipse.egit.ui.UIText;
39 import org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator;
40 import org.eclipse.egit.ui.internal.dialogs.CommitDialog;
41 import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
42 import org.eclipse.jface.action.IAction;
43 import org.eclipse.jface.dialogs.IDialogConstants;
44 import org.eclipse.jface.dialogs.MessageDialog;
45 import org.eclipse.jgit.lib.Commit;
46 import org.eclipse.jgit.lib.Constants;
47 import org.eclipse.jgit.lib.GitIndex;
48 import org.eclipse.jgit.lib.IndexDiff;
49 import org.eclipse.jgit.lib.ObjectId;
50 import org.eclipse.jgit.lib.PersonIdent;
51 import org.eclipse.jgit.lib.Repository;
52 import org.eclipse.jgit.lib.RepositoryConfig;
53 import org.eclipse.jgit.lib.RepositoryState;
54 import org.eclipse.jgit.lib.Tree;
55 import org.eclipse.jgit.lib.TreeEntry;
56 import org.eclipse.jgit.lib.GitIndex.Entry;
57 import org.eclipse.osgi.util.NLS;
58 import org.eclipse.team.core.Team;
59 import org.eclipse.team.core.TeamException;
60 import org.eclipse.team.internal.ui.Utils;
61 import org.eclipse.ui.PlatformUI;
63 /**
64 * Scan for modified resources in the same project as the selected resources.
66 public class CommitAction extends RepositoryAction {
68 private ArrayList<IFile> notIndexed;
69 private ArrayList<IFile> indexChanges;
70 private ArrayList<IFile> notTracked;
71 private ArrayList<IFile> files;
73 private Commit previousCommit;
75 private boolean amendAllowed;
76 private boolean amending;
78 @Override
79 public void execute(IAction act) {
80 // let's see if there is any dirty editor around and
81 // ask the user if they want to save or abort
82 if (!PlatformUI.getWorkbench().saveAllEditors(true)) {
83 return;
86 resetState();
87 try {
88 buildIndexHeadDiffList();
89 } catch (IOException e) {
90 handle(
91 new TeamException(UIText.CommitAction_errorComputingDiffs,
92 e), UIText.CommitAction_errorDuringCommit,
93 UIText.CommitAction_errorComputingDiffs);
94 return;
95 } catch (CoreException e) {
96 handle(
97 new TeamException(UIText.CommitAction_errorComputingDiffs,
98 e), UIText.CommitAction_errorDuringCommit,
99 UIText.CommitAction_errorComputingDiffs);
100 return;
103 Repository[] repos = getRepositoriesFor(getProjectsForSelectedResources());
104 Repository repository = null;
105 amendAllowed = repos.length == 1;
106 for (Repository repo : repos) {
107 repository = repo;
108 RepositoryState state = repo.getRepositoryState();
109 // currently we don't support committing a merge commit
110 if (state == RepositoryState.MERGING_RESOLVED || !state.canCommit()) {
111 MessageDialog.openError(getTargetPart().getSite().getShell(),
112 UIText.CommitAction_cannotCommit,
113 NLS.bind(UIText.CommitAction_repositoryState, state.getDescription()));
114 return;
118 loadPreviousCommit();
119 if (files.isEmpty()) {
120 if (amendAllowed && previousCommit != null) {
121 boolean result = MessageDialog
122 .openQuestion(getTargetPart().getSite().getShell(),
123 UIText.CommitAction_noFilesToCommit,
124 UIText.CommitAction_amendCommit);
125 if (!result)
126 return;
127 amending = true;
128 } else {
129 MessageDialog.openWarning(getTargetPart().getSite().getShell(), UIText.CommitAction_noFilesToCommit, UIText.CommitAction_amendNotPossible);
130 return;
134 String author = null;
135 String committer = null;
136 if (repository != null) {
137 final RepositoryConfig config = repository.getConfig();
138 author = config.getAuthorName();
139 final String authorEmail = config.getAuthorEmail();
140 author = author + " <" + authorEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
142 committer = config.getCommitterName();
143 final String committerEmail = config.getCommitterEmail();
144 committer = committer + " <" + committerEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
147 CommitDialog commitDialog = new CommitDialog(getTargetPart().getSite().getShell());
148 commitDialog.setAmending(amending);
149 commitDialog.setAmendAllowed(amendAllowed);
150 commitDialog.setFileList(files);
151 commitDialog.setPreselectedFiles(getSelectedFiles());
152 commitDialog.setAuthor(author);
153 commitDialog.setCommitter(committer);
155 if (previousCommit != null) {
156 commitDialog.setPreviousCommitMessage(previousCommit.getMessage());
157 PersonIdent previousAuthor = previousCommit.getAuthor();
158 commitDialog.setPreviousAuthor(previousAuthor.getName()
159 + " <" + previousAuthor.getEmailAddress() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
162 if (commitDialog.open() != IDialogConstants.OK_ID)
163 return;
165 final CommitOperation commitOperation = new CommitOperation(
166 commitDialog.getSelectedFiles(), notIndexed, notTracked,
167 commitDialog.getAuthor(), commitDialog.getCommitter(),
168 commitDialog.getCommitMessage());
169 if (commitDialog.isAmending()) {
170 commitOperation.setAmending(true);
171 commitOperation.setPreviousCommit(previousCommit);
172 commitOperation.setRepos(repos);
174 String jobname = UIText.CommitAction_CommittingChanges;
175 Job job = new Job(jobname) {
176 @Override
177 protected IStatus run(IProgressMonitor monitor) {
178 try {
179 commitOperation.execute(monitor);
181 for (IProject proj : getProjectsForSelectedResources()) {
182 RepositoryMapping.getMapping(proj).fireRepositoryChanged();
184 } catch (CoreException e) {
185 return Activator.createErrorStatus(
186 UIText.CommitAction_CommittingFailed, e);
187 } finally {
188 GitLightweightDecorator.refresh();
190 return Status.OK_STATUS;
193 job.setUser(true);
194 job.schedule();
197 private void resetState() {
198 files = new ArrayList<IFile>();
199 notIndexed = new ArrayList<IFile>();
200 indexChanges = new ArrayList<IFile>();
201 notTracked = new ArrayList<IFile>();
202 amending = false;
203 previousCommit = null;
207 * Retrieves a collection of files that may be committed based on the user's
208 * selection when they performed the commit action. That is, even if the
209 * user only selected one folder when the action was performed, if the
210 * folder contains any files that could be committed, they will be returned.
212 * @return a collection of files that is eligible to be committed based on
213 * the user's selection
215 private Collection<IFile> getSelectedFiles() {
216 List<IFile> preselectionCandidates = new ArrayList<IFile>();
217 // get the resources the user selected
218 IResource[] selectedResources = getSelectedResources();
219 // iterate through all the files that may be committed
220 for (IFile file : files) {
221 for (IResource resource : selectedResources) {
222 // if any selected resource contains the file, add it as a
223 // preselection candidate
224 if (resource.contains(file)) {
225 preselectionCandidates.add(file);
226 break;
230 return preselectionCandidates;
233 private void loadPreviousCommit() {
234 IProject project = getProjectsForSelectedResources()[0];
236 Repository repo = RepositoryMapping.getMapping(project).getRepository();
237 try {
238 ObjectId parentId = repo.resolve(Constants.HEAD);
239 if (parentId != null)
240 previousCommit = repo.mapCommit(parentId);
241 } catch (IOException e) {
242 Utils.handleError(getTargetPart().getSite().getShell(), e, UIText.CommitAction_errorDuringCommit, UIText.CommitAction_errorRetrievingCommit);
246 private void buildIndexHeadDiffList() throws IOException, CoreException {
247 HashMap<Repository, HashSet<IProject>> repositories = new HashMap<Repository, HashSet<IProject>>();
249 for (IProject project : getProjectsInRepositoryOfSelectedResources()) {
250 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
251 assert repositoryMapping != null;
253 Repository repository = repositoryMapping.getRepository();
255 HashSet<IProject> projects = repositories.get(repository);
257 if (projects == null) {
258 projects = new HashSet<IProject>();
259 repositories.put(repository, projects);
262 projects.add(project);
265 for (Map.Entry<Repository, HashSet<IProject>> entry : repositories.entrySet()) {
266 Repository repository = entry.getKey();
267 HashSet<IProject> projects = entry.getValue();
269 Tree head = repository.mapTree(Constants.HEAD);
270 GitIndex index = repository.getIndex();
271 IndexDiff indexDiff = new IndexDiff(head, index);
272 indexDiff.diff();
274 for (IProject project : projects) {
275 includeList(project, indexDiff.getAdded(), indexChanges);
276 includeList(project, indexDiff.getChanged(), indexChanges);
277 includeList(project, indexDiff.getRemoved(), indexChanges);
278 includeList(project, indexDiff.getMissing(), notIndexed);
279 includeList(project, indexDiff.getModified(), notIndexed);
280 addUntrackedFiles(repository, project);
285 private void addUntrackedFiles(final Repository repository, final IProject project) throws CoreException, IOException {
286 final GitIndex index = repository.getIndex();
287 final Tree headTree = repository.mapTree(Constants.HEAD);
288 project.accept(new IResourceVisitor() {
290 public boolean visit(IResource resource) throws CoreException {
291 if (Team.isIgnoredHint(resource))
292 return false;
293 if (resource.getType() == IResource.FILE) {
295 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(resource);
296 try {
297 TreeEntry headEntry = (headTree == null ? null : headTree.findBlobMember(repoRelativePath));
298 if (headEntry == null){
299 Entry indexEntry = null;
300 indexEntry = index.getEntry(repoRelativePath);
302 if (indexEntry == null) {
303 notTracked.add((IFile)resource);
304 files.add((IFile)resource);
307 } catch (IOException e) {
308 throw new TeamException(UIText.CommitAction_InternalError, e);
311 return true;
318 private void includeList(IProject project, HashSet<String> added, ArrayList<IFile> category) {
319 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(project);
320 if (repoRelativePath.length() > 0) {
321 repoRelativePath += "/"; //$NON-NLS-1$
324 for (String filename : added) {
325 try {
326 if (!filename.startsWith(repoRelativePath))
327 continue;
328 String projectRelativePath = filename.substring(repoRelativePath.length());
329 IResource member = project.getFile(projectRelativePath);
330 if (member != null && member instanceof IFile) {
331 if (!files.contains(member))
332 files.add((IFile) member);
333 category.add((IFile) member);
334 } else {
335 // TODO is this the right Location?
336 if (GitTraceLocation.UI.isActive())
337 GitTraceLocation.getTrace().trace(
338 GitTraceLocation.UI.getLocation(),
339 "Couldn't find " + filename); //$NON-NLS-1$
341 } catch (Exception e) {
342 if (GitTraceLocation.UI.isActive())
343 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
344 continue;
345 } // if it's outside the workspace, bad things happen
349 boolean tryAddResource(IFile resource, GitProjectData projectData, ArrayList<IFile> category) {
350 if (files.contains(resource))
351 return false;
353 try {
354 RepositoryMapping repositoryMapping = projectData
355 .getRepositoryMapping(resource);
357 if (isChanged(repositoryMapping, resource)) {
358 files.add(resource);
359 category.add(resource);
360 return true;
362 } catch (Exception e) {
363 if (GitTraceLocation.UI.isActive())
364 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
366 return false;
369 private boolean isChanged(RepositoryMapping map, IFile resource) {
370 try {
371 Repository repository = map.getRepository();
372 GitIndex index = repository.getIndex();
373 String repoRelativePath = map.getRepoRelativePath(resource);
374 Entry entry = index.getEntry(repoRelativePath);
375 if (entry != null)
376 return entry.isModified(map.getWorkDir());
377 return false;
378 } catch (UnsupportedEncodingException e) {
379 if (GitTraceLocation.UI.isActive())
380 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
381 } catch (IOException e) {
382 if (GitTraceLocation.UI.isActive())
383 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
385 return false;
388 @Override
389 public boolean isEnabled() {
390 return getProjectsInRepositoryOfSelectedResources().length > 0;