Show untracked files per default and remember last selection
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / actions / CommitAction.java
bloba92eab5e87a15311489a05b970b2a5698ec0c819
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;
62 /**
63 * Scan for modified resources in the same project as the selected resources.
65 public class CommitAction extends RepositoryAction {
67 private ArrayList<IFile> notIndexed;
68 private ArrayList<IFile> indexChanges;
69 private ArrayList<IFile> notTracked;
70 private ArrayList<IFile> files;
72 private Commit previousCommit;
74 private boolean amendAllowed;
75 private boolean amending;
77 @Override
78 public void execute(IAction act) {
79 resetState();
80 try {
81 buildIndexHeadDiffList();
82 } catch (IOException e) {
83 handle(
84 new TeamException(UIText.CommitAction_errorComputingDiffs,
85 e), UIText.CommitAction_errorDuringCommit,
86 UIText.CommitAction_errorComputingDiffs);
87 return;
88 } catch (CoreException e) {
89 handle(
90 new TeamException(UIText.CommitAction_errorComputingDiffs,
91 e), UIText.CommitAction_errorDuringCommit,
92 UIText.CommitAction_errorComputingDiffs);
93 return;
96 Repository[] repos = getRepositoriesFor(getProjectsForSelectedResources());
97 Repository repository = null;
98 amendAllowed = repos.length == 1;
99 for (Repository repo : repos) {
100 repository = repo;
101 RepositoryState state = repo.getRepositoryState();
102 // currently we don't support committing a merge commit
103 if (state == RepositoryState.MERGING_RESOLVED || !state.canCommit()) {
104 MessageDialog.openError(getTargetPart().getSite().getShell(),
105 UIText.CommitAction_cannotCommit,
106 NLS.bind(UIText.CommitAction_repositoryState, state.getDescription()));
107 return;
111 loadPreviousCommit();
112 if (files.isEmpty()) {
113 if (amendAllowed && previousCommit != null) {
114 boolean result = MessageDialog
115 .openQuestion(getTargetPart().getSite().getShell(),
116 UIText.CommitAction_noFilesToCommit,
117 UIText.CommitAction_amendCommit);
118 if (!result)
119 return;
120 amending = true;
121 } else {
122 MessageDialog.openWarning(getTargetPart().getSite().getShell(), UIText.CommitAction_noFilesToCommit, UIText.CommitAction_amendNotPossible);
123 return;
127 String author = null;
128 String committer = null;
129 if (repository != null) {
130 final RepositoryConfig config = repository.getConfig();
131 author = config.getAuthorName();
132 final String authorEmail = config.getAuthorEmail();
133 author = author + " <" + authorEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
135 committer = config.getCommitterName();
136 final String committerEmail = config.getCommitterEmail();
137 committer = committer + " <" + committerEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
140 CommitDialog commitDialog = new CommitDialog(getTargetPart().getSite().getShell());
141 commitDialog.setAmending(amending);
142 commitDialog.setAmendAllowed(amendAllowed);
143 commitDialog.setFileList(files);
144 commitDialog.setPreselectedFiles(getSelectedFiles());
145 commitDialog.setAuthor(author);
146 commitDialog.setCommitter(committer);
148 if (previousCommit != null) {
149 commitDialog.setPreviousCommitMessage(previousCommit.getMessage());
150 PersonIdent previousAuthor = previousCommit.getAuthor();
151 commitDialog.setPreviousAuthor(previousAuthor.getName()
152 + " <" + previousAuthor.getEmailAddress() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
155 if (commitDialog.open() != IDialogConstants.OK_ID)
156 return;
158 final CommitOperation commitOperation = new CommitOperation(
159 commitDialog.getSelectedFiles(), notIndexed, notTracked,
160 commitDialog.getAuthor(), commitDialog.getCommitter(),
161 commitDialog.getCommitMessage());
162 if (commitDialog.isAmending()) {
163 commitOperation.setAmending(true);
164 commitOperation.setPreviousCommit(previousCommit);
165 commitOperation.setRepos(repos);
167 String jobname = UIText.CommitAction_CommittingChanges;
168 Job job = new Job(jobname) {
169 @Override
170 protected IStatus run(IProgressMonitor monitor) {
171 try {
172 commitOperation.execute(monitor);
174 for (IProject proj : getProjectsForSelectedResources()) {
175 RepositoryMapping.getMapping(proj).fireRepositoryChanged();
177 } catch (CoreException e) {
178 return Activator.createErrorStatus(
179 UIText.CommitAction_CommittingFailed, e);
180 } finally {
181 GitLightweightDecorator.refresh();
183 return Status.OK_STATUS;
186 job.setUser(true);
187 job.schedule();
190 private void resetState() {
191 files = new ArrayList<IFile>();
192 notIndexed = new ArrayList<IFile>();
193 indexChanges = new ArrayList<IFile>();
194 notTracked = new ArrayList<IFile>();
195 amending = false;
196 previousCommit = null;
200 * Retrieves a collection of files that may be committed based on the user's
201 * selection when they performed the commit action. That is, even if the
202 * user only selected one folder when the action was performed, if the
203 * folder contains any files that could be committed, they will be returned.
205 * @return a collection of files that is eligible to be committed based on
206 * the user's selection
208 private Collection<IFile> getSelectedFiles() {
209 List<IFile> preselectionCandidates = new ArrayList<IFile>();
210 // get the resources the user selected
211 IResource[] selectedResources = getSelectedResources();
212 // iterate through all the files that may be committed
213 for (IFile file : files) {
214 for (IResource resource : selectedResources) {
215 // if any selected resource contains the file, add it as a
216 // preselection candidate
217 if (resource.contains(file)) {
218 preselectionCandidates.add(file);
219 break;
223 return preselectionCandidates;
226 private void loadPreviousCommit() {
227 IProject project = getProjectsForSelectedResources()[0];
229 Repository repo = RepositoryMapping.getMapping(project).getRepository();
230 try {
231 ObjectId parentId = repo.resolve(Constants.HEAD);
232 if (parentId != null)
233 previousCommit = repo.mapCommit(parentId);
234 } catch (IOException e) {
235 Utils.handleError(getTargetPart().getSite().getShell(), e, UIText.CommitAction_errorDuringCommit, UIText.CommitAction_errorRetrievingCommit);
239 private void buildIndexHeadDiffList() throws IOException, CoreException {
240 HashMap<Repository, HashSet<IProject>> repositories = new HashMap<Repository, HashSet<IProject>>();
242 for (IProject project : getProjectsInRepositoryOfSelectedResources()) {
243 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
244 assert repositoryMapping != null;
246 Repository repository = repositoryMapping.getRepository();
248 HashSet<IProject> projects = repositories.get(repository);
250 if (projects == null) {
251 projects = new HashSet<IProject>();
252 repositories.put(repository, projects);
255 projects.add(project);
258 for (Map.Entry<Repository, HashSet<IProject>> entry : repositories.entrySet()) {
259 Repository repository = entry.getKey();
260 HashSet<IProject> projects = entry.getValue();
262 Tree head = repository.mapTree(Constants.HEAD);
263 GitIndex index = repository.getIndex();
264 IndexDiff indexDiff = new IndexDiff(head, index);
265 indexDiff.diff();
267 for (IProject project : projects) {
268 includeList(project, indexDiff.getAdded(), indexChanges);
269 includeList(project, indexDiff.getChanged(), indexChanges);
270 includeList(project, indexDiff.getRemoved(), indexChanges);
271 includeList(project, indexDiff.getMissing(), notIndexed);
272 includeList(project, indexDiff.getModified(), notIndexed);
273 addUntrackedFiles(repository, project);
278 private void addUntrackedFiles(final Repository repository, final IProject project) throws CoreException, IOException {
279 final GitIndex index = repository.getIndex();
280 final Tree headTree = repository.mapTree(Constants.HEAD);
281 project.accept(new IResourceVisitor() {
283 public boolean visit(IResource resource) throws CoreException {
284 if (Team.isIgnoredHint(resource))
285 return false;
286 if (resource.getType() == IResource.FILE) {
288 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(resource);
289 try {
290 TreeEntry headEntry = (headTree == null ? null : headTree.findBlobMember(repoRelativePath));
291 if (headEntry == null){
292 Entry indexEntry = null;
293 indexEntry = index.getEntry(repoRelativePath);
295 if (indexEntry == null) {
296 notTracked.add((IFile)resource);
297 files.add((IFile)resource);
300 } catch (IOException e) {
301 throw new TeamException(UIText.CommitAction_InternalError, e);
304 return true;
311 private void includeList(IProject project, HashSet<String> added, ArrayList<IFile> category) {
312 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(project);
313 if (repoRelativePath.length() > 0) {
314 repoRelativePath += "/"; //$NON-NLS-1$
317 for (String filename : added) {
318 try {
319 if (!filename.startsWith(repoRelativePath))
320 continue;
321 String projectRelativePath = filename.substring(repoRelativePath.length());
322 IResource member = project.getFile(projectRelativePath);
323 if (member != null && member instanceof IFile) {
324 if (!files.contains(member))
325 files.add((IFile) member);
326 category.add((IFile) member);
327 } else {
328 // TODO is this the right Location?
329 if (GitTraceLocation.UI.isActive())
330 GitTraceLocation.getTrace().trace(
331 GitTraceLocation.UI.getLocation(),
332 "Couldn't find " + filename); //$NON-NLS-1$
334 } catch (Exception e) {
335 if (GitTraceLocation.UI.isActive())
336 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
337 continue;
338 } // if it's outside the workspace, bad things happen
342 boolean tryAddResource(IFile resource, GitProjectData projectData, ArrayList<IFile> category) {
343 if (files.contains(resource))
344 return false;
346 try {
347 RepositoryMapping repositoryMapping = projectData
348 .getRepositoryMapping(resource);
350 if (isChanged(repositoryMapping, resource)) {
351 files.add(resource);
352 category.add(resource);
353 return true;
355 } catch (Exception e) {
356 if (GitTraceLocation.UI.isActive())
357 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
359 return false;
362 private boolean isChanged(RepositoryMapping map, IFile resource) {
363 try {
364 Repository repository = map.getRepository();
365 GitIndex index = repository.getIndex();
366 String repoRelativePath = map.getRepoRelativePath(resource);
367 Entry entry = index.getEntry(repoRelativePath);
368 if (entry != null)
369 return entry.isModified(map.getWorkDir());
370 return false;
371 } catch (UnsupportedEncodingException e) {
372 if (GitTraceLocation.UI.isActive())
373 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
374 } catch (IOException e) {
375 if (GitTraceLocation.UI.isActive())
376 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
378 return false;
381 @Override
382 public boolean isEnabled() {
383 return getProjectsInRepositoryOfSelectedResources().length > 0;