Improve DiscardChangesAction
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / actions / CommitAction.java
blob7ad409439761963a21f12f6ece59b596804708a9
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>
8 * All rights reserved. This program and the accompanying materials
9 * are made available under the terms of the Eclipse Public License v1.0
10 * which accompanies this distribution, and is available at
11 * http://www.eclipse.org/legal/epl-v10.html
12 *******************************************************************************/
13 package org.eclipse.egit.ui.internal.actions;
15 import java.io.File;
16 import java.io.IOException;
17 import java.io.UnsupportedEncodingException;
18 import java.util.ArrayList;
19 import java.util.Date;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.TimeZone;
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.egit.core.project.GitProjectData;
31 import org.eclipse.egit.core.project.RepositoryMapping;
32 import org.eclipse.egit.ui.UIText;
33 import org.eclipse.egit.ui.internal.dialogs.CommitDialog;
34 import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
35 import org.eclipse.jface.action.IAction;
36 import org.eclipse.jface.dialogs.IDialogConstants;
37 import org.eclipse.jface.dialogs.MessageDialog;
38 import org.eclipse.jgit.lib.Commit;
39 import org.eclipse.jgit.lib.Constants;
40 import org.eclipse.jgit.lib.GitIndex;
41 import org.eclipse.jgit.lib.IndexDiff;
42 import org.eclipse.jgit.lib.ObjectId;
43 import org.eclipse.jgit.lib.ObjectWriter;
44 import org.eclipse.jgit.lib.PersonIdent;
45 import org.eclipse.jgit.lib.RefUpdate;
46 import org.eclipse.jgit.lib.Repository;
47 import org.eclipse.jgit.lib.RepositoryConfig;
48 import org.eclipse.jgit.lib.Tree;
49 import org.eclipse.jgit.lib.TreeEntry;
50 import org.eclipse.jgit.lib.GitIndex.Entry;
51 import org.eclipse.osgi.util.NLS;
52 import org.eclipse.team.core.Team;
53 import org.eclipse.team.core.TeamException;
54 import org.eclipse.team.internal.ui.Utils;
56 /**
57 * Scan for modified resources in the same project as the selected resources.
59 public class CommitAction extends RepositoryAction {
61 private ArrayList<IFile> notIndexed;
62 private ArrayList<IFile> indexChanges;
63 private ArrayList<IFile> notTracked;
64 private ArrayList<IFile> files;
66 private Commit previousCommit;
68 private boolean amendAllowed;
69 private boolean amending;
71 @Override
72 public void execute(IAction act) {
73 resetState();
74 try {
75 buildIndexHeadDiffList();
76 } catch (IOException e) {
77 handle(
78 new TeamException(UIText.CommitAction_errorComputingDiffs,
79 e), UIText.CommitAction_errorDuringCommit,
80 UIText.CommitAction_errorComputingDiffs);
81 return;
82 } catch (CoreException e) {
83 handle(
84 new TeamException(UIText.CommitAction_errorComputingDiffs,
85 e), UIText.CommitAction_errorDuringCommit,
86 UIText.CommitAction_errorComputingDiffs);
87 return;
90 Repository[] repos = getRepositoriesFor(getProjectsForSelectedResources());
91 Repository repository = null;
92 amendAllowed = repos.length == 1;
93 for (Repository repo : repos) {
94 repository = repo;
95 if (!repo.getRepositoryState().canCommit()) {
96 MessageDialog.openError(getTargetPart().getSite().getShell(),
97 UIText.CommitAction_cannotCommit,
98 NLS.bind(UIText.CommitAction_repositoryState, repo.getRepositoryState().getDescription()));
99 return;
103 loadPreviousCommit();
104 if (files.isEmpty()) {
105 if (amendAllowed && previousCommit != null) {
106 boolean result = MessageDialog
107 .openQuestion(getTargetPart().getSite().getShell(),
108 UIText.CommitAction_noFilesToCommit,
109 UIText.CommitAction_amendCommit);
110 if (!result)
111 return;
112 amending = true;
113 } else {
114 MessageDialog.openWarning(getTargetPart().getSite().getShell(), UIText.CommitAction_noFilesToCommit, UIText.CommitAction_amendNotPossible);
115 return;
119 String author = null;
120 String committer = null;
121 if (repository != null) {
122 final RepositoryConfig config = repository.getConfig();
123 author = config.getAuthorName();
124 final String authorEmail = config.getAuthorEmail();
125 author = author + " <" + authorEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
127 committer = config.getCommitterName();
128 final String committerEmail = config.getCommitterEmail();
129 committer = committer + " <" + committerEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
132 CommitDialog commitDialog = new CommitDialog(getTargetPart().getSite().getShell());
133 commitDialog.setAmending(amending);
134 commitDialog.setAmendAllowed(amendAllowed);
135 commitDialog.setFileList(files);
136 commitDialog.setAuthor(author);
137 commitDialog.setCommitter(committer);
138 if(notTracked.size() == files.size())
139 commitDialog.setShowUntracked(true);
141 if (previousCommit != null) {
142 commitDialog.setPreviousCommitMessage(previousCommit.getMessage());
143 PersonIdent previousAuthor = previousCommit.getAuthor();
144 commitDialog.setPreviousAuthor(previousAuthor.getName() + " <" + previousAuthor.getEmailAddress() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
147 if (commitDialog.open() != IDialogConstants.OK_ID)
148 return;
150 String commitMessage = commitDialog.getCommitMessage();
151 amending = commitDialog.isAmending();
152 try {
153 performCommit(commitDialog, commitMessage);
154 } catch (TeamException e) {
155 handle(e, UIText.CommitAction_errorDuringCommit,
156 UIText.CommitAction_errorOnCommit);
160 private void resetState() {
161 files = new ArrayList<IFile>();
162 notIndexed = new ArrayList<IFile>();
163 indexChanges = new ArrayList<IFile>();
164 notTracked = new ArrayList<IFile>();
165 amending = false;
166 previousCommit = null;
169 private void loadPreviousCommit() {
170 IProject project = getProjectsForSelectedResources()[0];
172 Repository repo = RepositoryMapping.getMapping(project).getRepository();
173 try {
174 ObjectId parentId = repo.resolve(Constants.HEAD);
175 if (parentId != null)
176 previousCommit = repo.mapCommit(parentId);
177 } catch (IOException e) {
178 Utils.handleError(getTargetPart().getSite().getShell(), e, UIText.CommitAction_errorDuringCommit, UIText.CommitAction_errorRetrievingCommit);
182 private void performCommit(CommitDialog commitDialog, String commitMessage)
183 throws TeamException {
185 IFile[] selectedItems = commitDialog.getSelectedFiles();
187 HashMap<Repository, Tree> treeMap = new HashMap<Repository, Tree>();
188 try {
189 prepareTrees(selectedItems, treeMap);
190 } catch (IOException e) {
191 throw new TeamException(UIText.CommitAction_errorPreparingTrees, e);
194 try {
195 doCommits(commitDialog, commitMessage, treeMap);
196 } catch (IOException e) {
197 throw new TeamException(UIText.CommitAction_errorCommittingChanges, e);
199 for (IProject proj : getProjectsForSelectedResources()) {
200 RepositoryMapping.getMapping(proj).fireRepositoryChanged();
204 private void doCommits(CommitDialog commitDialog, String commitMessage,
205 HashMap<Repository, Tree> treeMap) throws IOException, TeamException {
207 final String author = commitDialog.getAuthor();
208 final String committer = commitDialog.getCommitter();
209 final Date commitDate = new Date();
210 final TimeZone timeZone = TimeZone.getDefault();
212 final PersonIdent authorIdent = new PersonIdent(author);
213 final PersonIdent committerIdent = new PersonIdent(committer);
215 for (java.util.Map.Entry<Repository, Tree> entry : treeMap.entrySet()) {
216 Tree tree = entry.getValue();
217 Repository repo = tree.getRepository();
218 writeTreeWithSubTrees(tree);
220 ObjectId currentHeadId = repo.resolve(Constants.HEAD);
221 ObjectId[] parentIds;
222 if (amending) {
223 parentIds = previousCommit.getParentIds();
224 } else {
225 if (currentHeadId != null)
226 parentIds = new ObjectId[] { currentHeadId };
227 else
228 parentIds = new ObjectId[0];
230 Commit commit = new Commit(repo, parentIds);
231 commit.setTree(tree);
232 commit.setMessage(commitMessage);
233 commit.setAuthor(new PersonIdent(authorIdent, commitDate, timeZone));
234 commit.setCommitter(new PersonIdent(committerIdent, commitDate, timeZone));
236 ObjectWriter writer = new ObjectWriter(repo);
237 commit.setCommitId(writer.writeCommit(commit));
239 final RefUpdate ru = repo.updateRef(Constants.HEAD);
240 ru.setNewObjectId(commit.getCommitId());
241 ru.setRefLogMessage(buildReflogMessage(commitMessage), false);
242 if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE) {
243 throw new TeamException(
244 NLS.bind(UIText.CommitAction_failedToUpdate, ru.getName(),
245 commit.getCommitId()));
250 private void prepareTrees(IFile[] selectedItems,
251 HashMap<Repository, Tree> treeMap) throws IOException,
252 UnsupportedEncodingException {
253 if (selectedItems.length == 0) {
254 // amending commit - need to put something into the map
255 for (IProject proj : getProjectsForSelectedResources()) {
256 Repository repo = RepositoryMapping.getMapping(proj).getRepository();
257 if (!treeMap.containsKey(repo))
258 treeMap.put(repo, repo.mapTree(Constants.HEAD));
262 for (IFile file : selectedItems) {
264 IProject project = file.getProject();
265 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
266 Repository repository = repositoryMapping.getRepository();
267 Tree projTree = treeMap.get(repository);
268 if (projTree == null) {
269 projTree = repository.mapTree(Constants.HEAD);
270 if (projTree == null)
271 projTree = new Tree(repository);
272 treeMap.put(repository, projTree);
273 // TODO is this the right Location?
274 if (GitTraceLocation.UI.isActive())
275 GitTraceLocation.getTrace().trace(
276 GitTraceLocation.UI.getLocation(),
277 "Orig tree id: " + projTree.getId()); //$NON-NLS-1$
279 GitIndex index = repository.getIndex();
280 String repoRelativePath = repositoryMapping
281 .getRepoRelativePath(file);
282 String string = repoRelativePath;
284 TreeEntry treeMember = projTree.findBlobMember(repoRelativePath);
285 // we always want to delete it from the current tree, since if it's
286 // updated, we'll add it again
287 if (treeMember != null)
288 treeMember.delete();
290 Entry idxEntry = index.getEntry(string);
291 if (notIndexed.contains(file)) {
292 File thisfile = new File(repositoryMapping.getWorkDir(), idxEntry.getName());
293 if (!thisfile.isFile()) {
294 index.remove(repositoryMapping.getWorkDir(), thisfile);
295 index.write();
296 // TODO is this the right Location?
297 if (GitTraceLocation.UI.isActive())
298 GitTraceLocation.getTrace().trace(
299 GitTraceLocation.UI.getLocation(),
300 "Phantom file, so removing from index"); //$NON-NLS-1$
301 continue;
302 } else {
303 if (idxEntry.update(thisfile))
304 index.write();
307 if (notTracked.contains(file)) {
308 idxEntry = index.add(repositoryMapping.getWorkDir(), new File(repositoryMapping.getWorkDir(),
309 repoRelativePath));
310 index.write();
315 if (idxEntry != null) {
316 projTree.addFile(repoRelativePath);
317 TreeEntry newMember = projTree.findBlobMember(repoRelativePath);
319 newMember.setId(idxEntry.getObjectId());
320 // TODO is this the right Location?
321 if (GitTraceLocation.UI.isActive())
322 GitTraceLocation.getTrace().trace(
323 GitTraceLocation.UI.getLocation(),
324 "New member id for " + repoRelativePath //$NON-NLS-1$
325 + ": " + newMember.getId() + " idx id: " //$NON-NLS-1$ //$NON-NLS-2$
326 + idxEntry.getObjectId());
331 private String buildReflogMessage(String commitMessage) {
332 String firstLine = commitMessage;
333 int newlineIndex = commitMessage.indexOf("\n"); //$NON-NLS-1$
334 if (newlineIndex > 0) {
335 firstLine = commitMessage.substring(0, newlineIndex);
337 String commitStr = amending ? "commit (amend):" : "commit: "; //$NON-NLS-1$ //$NON-NLS-2$
338 String message = commitStr + firstLine;
339 return message;
342 private void writeTreeWithSubTrees(Tree tree) throws TeamException {
343 if (tree.getId() == null) {
344 // TODO is this the right Location?
345 if (GitTraceLocation.UI.isActive())
346 GitTraceLocation.getTrace().trace(
347 GitTraceLocation.UI.getLocation(),
348 "writing tree for: " + tree.getFullName()); //$NON-NLS-1$
349 try {
350 for (TreeEntry entry : tree.members()) {
351 if (entry.isModified()) {
352 if (entry instanceof Tree) {
353 writeTreeWithSubTrees((Tree) entry);
354 } else {
355 // this shouldn't happen.... not quite sure what to
356 // do here :)
357 // TODO is this the right Location?
358 if (GitTraceLocation.UI.isActive())
359 GitTraceLocation.getTrace().trace(
360 GitTraceLocation.UI.getLocation(),
361 "BAD JUJU: " //$NON-NLS-1$
362 + entry.getFullName());
366 ObjectWriter writer = new ObjectWriter(tree.getRepository());
367 tree.setId(writer.writeTree(tree));
368 } catch (IOException e) {
369 throw new TeamException(UIText.CommitAction_errorWritingTrees, e);
374 private void buildIndexHeadDiffList() throws IOException, CoreException {
375 HashMap<Repository, HashSet<IProject>> repositories = new HashMap<Repository, HashSet<IProject>>();
377 for (IProject project : getProjectsInRepositoryOfSelectedResources()) {
378 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
379 assert repositoryMapping != null;
381 Repository repository = repositoryMapping.getRepository();
383 HashSet<IProject> projects = repositories.get(repository);
385 if (projects == null) {
386 projects = new HashSet<IProject>();
387 repositories.put(repository, projects);
390 projects.add(project);
393 for (Map.Entry<Repository, HashSet<IProject>> entry : repositories.entrySet()) {
394 Repository repository = entry.getKey();
395 HashSet<IProject> projects = entry.getValue();
397 Tree head = repository.mapTree(Constants.HEAD);
398 GitIndex index = repository.getIndex();
399 IndexDiff indexDiff = new IndexDiff(head, index);
400 indexDiff.diff();
402 for (IProject project : projects) {
403 includeList(project, indexDiff.getAdded(), indexChanges);
404 includeList(project, indexDiff.getChanged(), indexChanges);
405 includeList(project, indexDiff.getRemoved(), indexChanges);
406 includeList(project, indexDiff.getMissing(), notIndexed);
407 includeList(project, indexDiff.getModified(), notIndexed);
408 addUntrackedFiles(repository, project);
413 private void addUntrackedFiles(final Repository repository, final IProject project) throws CoreException, IOException {
414 final GitIndex index = repository.getIndex();
415 final Tree headTree = repository.mapTree(Constants.HEAD);
416 project.accept(new IResourceVisitor() {
418 public boolean visit(IResource resource) throws CoreException {
419 if (Team.isIgnoredHint(resource))
420 return false;
421 if (resource.getType() == IResource.FILE) {
423 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(resource);
424 try {
425 TreeEntry headEntry = (headTree == null ? null : headTree.findBlobMember(repoRelativePath));
426 if (headEntry == null){
427 Entry indexEntry = null;
428 indexEntry = index.getEntry(repoRelativePath);
430 if (indexEntry == null) {
431 notTracked.add((IFile)resource);
432 files.add((IFile)resource);
435 } catch (IOException e) {
436 throw new TeamException(UIText.CommitAction_InternalError, e);
439 return true;
446 private void includeList(IProject project, HashSet<String> added, ArrayList<IFile> category) {
447 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(project);
448 if (repoRelativePath.length() > 0) {
449 repoRelativePath += "/"; //$NON-NLS-1$
452 for (String filename : added) {
453 try {
454 if (!filename.startsWith(repoRelativePath))
455 continue;
456 String projectRelativePath = filename.substring(repoRelativePath.length());
457 IResource member = project.getFile(projectRelativePath);
458 if (member != null && member instanceof IFile) {
459 if (!files.contains(member))
460 files.add((IFile) member);
461 category.add((IFile) member);
462 } else {
463 // TODO is this the right Location?
464 if (GitTraceLocation.UI.isActive())
465 GitTraceLocation.getTrace().trace(
466 GitTraceLocation.UI.getLocation(),
467 "Couldn't find " + filename); //$NON-NLS-1$
469 } catch (Exception e) {
470 if (GitTraceLocation.UI.isActive())
471 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
472 continue;
473 } // if it's outside the workspace, bad things happen
477 boolean tryAddResource(IFile resource, GitProjectData projectData, ArrayList<IFile> category) {
478 if (files.contains(resource))
479 return false;
481 try {
482 RepositoryMapping repositoryMapping = projectData
483 .getRepositoryMapping(resource);
485 if (isChanged(repositoryMapping, resource)) {
486 files.add(resource);
487 category.add(resource);
488 return true;
490 } catch (Exception e) {
491 if (GitTraceLocation.UI.isActive())
492 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
494 return false;
497 private boolean isChanged(RepositoryMapping map, IFile resource) {
498 try {
499 Repository repository = map.getRepository();
500 GitIndex index = repository.getIndex();
501 String repoRelativePath = map.getRepoRelativePath(resource);
502 Entry entry = index.getEntry(repoRelativePath);
503 if (entry != null)
504 return entry.isModified(map.getWorkDir());
505 return false;
506 } catch (UnsupportedEncodingException e) {
507 if (GitTraceLocation.UI.isActive())
508 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
509 } catch (IOException e) {
510 if (GitTraceLocation.UI.isActive())
511 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
513 return false;
516 @Override
517 public boolean isEnabled() {
518 return getProjectsInRepositoryOfSelectedResources().length > 0;