Prepare EGit for the new State MERGING_RESOLVED
[egit/spearce.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / actions / CommitAction.java
blob3038e8ee3129a3114b560ebfd34d0d9db9cdbbf4
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.RepositoryState;
49 import org.eclipse.jgit.lib.Tree;
50 import org.eclipse.jgit.lib.TreeEntry;
51 import org.eclipse.jgit.lib.GitIndex.Entry;
52 import org.eclipse.osgi.util.NLS;
53 import org.eclipse.team.core.Team;
54 import org.eclipse.team.core.TeamException;
55 import org.eclipse.team.internal.ui.Utils;
57 /**
58 * Scan for modified resources in the same project as the selected resources.
60 public class CommitAction extends RepositoryAction {
62 private ArrayList<IFile> notIndexed;
63 private ArrayList<IFile> indexChanges;
64 private ArrayList<IFile> notTracked;
65 private ArrayList<IFile> files;
67 private Commit previousCommit;
69 private boolean amendAllowed;
70 private boolean amending;
72 @Override
73 public void execute(IAction act) {
74 resetState();
75 try {
76 buildIndexHeadDiffList();
77 } catch (IOException e) {
78 handle(
79 new TeamException(UIText.CommitAction_errorComputingDiffs,
80 e), UIText.CommitAction_errorDuringCommit,
81 UIText.CommitAction_errorComputingDiffs);
82 return;
83 } catch (CoreException e) {
84 handle(
85 new TeamException(UIText.CommitAction_errorComputingDiffs,
86 e), UIText.CommitAction_errorDuringCommit,
87 UIText.CommitAction_errorComputingDiffs);
88 return;
91 Repository[] repos = getRepositoriesFor(getProjectsForSelectedResources());
92 Repository repository = null;
93 amendAllowed = repos.length == 1;
94 for (Repository repo : repos) {
95 repository = repo;
96 RepositoryState state = repo.getRepositoryState();
97 // currently we don't support committing a merge commit
98 if (state == RepositoryState.MERGING_RESOLVED || !state.canCommit()) {
99 MessageDialog.openError(getTargetPart().getSite().getShell(),
100 UIText.CommitAction_cannotCommit,
101 NLS.bind(UIText.CommitAction_repositoryState, state.getDescription()));
102 return;
106 loadPreviousCommit();
107 if (files.isEmpty()) {
108 if (amendAllowed && previousCommit != null) {
109 boolean result = MessageDialog
110 .openQuestion(getTargetPart().getSite().getShell(),
111 UIText.CommitAction_noFilesToCommit,
112 UIText.CommitAction_amendCommit);
113 if (!result)
114 return;
115 amending = true;
116 } else {
117 MessageDialog.openWarning(getTargetPart().getSite().getShell(), UIText.CommitAction_noFilesToCommit, UIText.CommitAction_amendNotPossible);
118 return;
122 String author = null;
123 String committer = null;
124 if (repository != null) {
125 final RepositoryConfig config = repository.getConfig();
126 author = config.getAuthorName();
127 final String authorEmail = config.getAuthorEmail();
128 author = author + " <" + authorEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
130 committer = config.getCommitterName();
131 final String committerEmail = config.getCommitterEmail();
132 committer = committer + " <" + committerEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
135 CommitDialog commitDialog = new CommitDialog(getTargetPart().getSite().getShell());
136 commitDialog.setAmending(amending);
137 commitDialog.setAmendAllowed(amendAllowed);
138 commitDialog.setFileList(files);
139 commitDialog.setAuthor(author);
140 commitDialog.setCommitter(committer);
141 if(notTracked.size() == files.size())
142 commitDialog.setShowUntracked(true);
144 if (previousCommit != null) {
145 commitDialog.setPreviousCommitMessage(previousCommit.getMessage());
146 PersonIdent previousAuthor = previousCommit.getAuthor();
147 commitDialog.setPreviousAuthor(previousAuthor.getName() + " <" + previousAuthor.getEmailAddress() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
150 if (commitDialog.open() != IDialogConstants.OK_ID)
151 return;
153 String commitMessage = commitDialog.getCommitMessage();
154 amending = commitDialog.isAmending();
155 try {
156 performCommit(commitDialog, commitMessage);
157 } catch (TeamException e) {
158 handle(e, UIText.CommitAction_errorDuringCommit,
159 UIText.CommitAction_errorOnCommit);
163 private void resetState() {
164 files = new ArrayList<IFile>();
165 notIndexed = new ArrayList<IFile>();
166 indexChanges = new ArrayList<IFile>();
167 notTracked = new ArrayList<IFile>();
168 amending = false;
169 previousCommit = null;
172 private void loadPreviousCommit() {
173 IProject project = getProjectsForSelectedResources()[0];
175 Repository repo = RepositoryMapping.getMapping(project).getRepository();
176 try {
177 ObjectId parentId = repo.resolve(Constants.HEAD);
178 if (parentId != null)
179 previousCommit = repo.mapCommit(parentId);
180 } catch (IOException e) {
181 Utils.handleError(getTargetPart().getSite().getShell(), e, UIText.CommitAction_errorDuringCommit, UIText.CommitAction_errorRetrievingCommit);
185 private void performCommit(CommitDialog commitDialog, String commitMessage)
186 throws TeamException {
188 IFile[] selectedItems = commitDialog.getSelectedFiles();
190 HashMap<Repository, Tree> treeMap = new HashMap<Repository, Tree>();
191 try {
192 prepareTrees(selectedItems, treeMap);
193 } catch (IOException e) {
194 throw new TeamException(UIText.CommitAction_errorPreparingTrees, e);
197 try {
198 doCommits(commitDialog, commitMessage, treeMap);
199 } catch (IOException e) {
200 throw new TeamException(UIText.CommitAction_errorCommittingChanges, e);
202 for (IProject proj : getProjectsForSelectedResources()) {
203 RepositoryMapping.getMapping(proj).fireRepositoryChanged();
207 private void doCommits(CommitDialog commitDialog, String commitMessage,
208 HashMap<Repository, Tree> treeMap) throws IOException, TeamException {
210 final String author = commitDialog.getAuthor();
211 final String committer = commitDialog.getCommitter();
212 final Date commitDate = new Date();
213 final TimeZone timeZone = TimeZone.getDefault();
215 final PersonIdent authorIdent = new PersonIdent(author);
216 final PersonIdent committerIdent = new PersonIdent(committer);
218 for (java.util.Map.Entry<Repository, Tree> entry : treeMap.entrySet()) {
219 Tree tree = entry.getValue();
220 Repository repo = tree.getRepository();
221 writeTreeWithSubTrees(tree);
223 ObjectId currentHeadId = repo.resolve(Constants.HEAD);
224 ObjectId[] parentIds;
225 if (amending) {
226 parentIds = previousCommit.getParentIds();
227 } else {
228 if (currentHeadId != null)
229 parentIds = new ObjectId[] { currentHeadId };
230 else
231 parentIds = new ObjectId[0];
233 Commit commit = new Commit(repo, parentIds);
234 commit.setTree(tree);
235 commit.setMessage(commitMessage);
236 commit.setAuthor(new PersonIdent(authorIdent, commitDate, timeZone));
237 commit.setCommitter(new PersonIdent(committerIdent, commitDate, timeZone));
239 ObjectWriter writer = new ObjectWriter(repo);
240 commit.setCommitId(writer.writeCommit(commit));
242 final RefUpdate ru = repo.updateRef(Constants.HEAD);
243 ru.setNewObjectId(commit.getCommitId());
244 ru.setRefLogMessage(buildReflogMessage(commitMessage), false);
245 if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE) {
246 throw new TeamException(
247 NLS.bind(UIText.CommitAction_failedToUpdate, ru.getName(),
248 commit.getCommitId()));
253 private void prepareTrees(IFile[] selectedItems,
254 HashMap<Repository, Tree> treeMap) throws IOException,
255 UnsupportedEncodingException {
256 if (selectedItems.length == 0) {
257 // amending commit - need to put something into the map
258 for (IProject proj : getProjectsForSelectedResources()) {
259 Repository repo = RepositoryMapping.getMapping(proj).getRepository();
260 if (!treeMap.containsKey(repo))
261 treeMap.put(repo, repo.mapTree(Constants.HEAD));
265 for (IFile file : selectedItems) {
267 IProject project = file.getProject();
268 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
269 Repository repository = repositoryMapping.getRepository();
270 Tree projTree = treeMap.get(repository);
271 if (projTree == null) {
272 projTree = repository.mapTree(Constants.HEAD);
273 if (projTree == null)
274 projTree = new Tree(repository);
275 treeMap.put(repository, projTree);
276 // TODO is this the right Location?
277 if (GitTraceLocation.UI.isActive())
278 GitTraceLocation.getTrace().trace(
279 GitTraceLocation.UI.getLocation(),
280 "Orig tree id: " + projTree.getId()); //$NON-NLS-1$
282 GitIndex index = repository.getIndex();
283 String repoRelativePath = repositoryMapping
284 .getRepoRelativePath(file);
285 String string = repoRelativePath;
287 TreeEntry treeMember = projTree.findBlobMember(repoRelativePath);
288 // we always want to delete it from the current tree, since if it's
289 // updated, we'll add it again
290 if (treeMember != null)
291 treeMember.delete();
293 Entry idxEntry = index.getEntry(string);
294 if (notIndexed.contains(file)) {
295 File thisfile = new File(repositoryMapping.getWorkDir(), idxEntry.getName());
296 if (!thisfile.isFile()) {
297 index.remove(repositoryMapping.getWorkDir(), thisfile);
298 index.write();
299 // TODO is this the right Location?
300 if (GitTraceLocation.UI.isActive())
301 GitTraceLocation.getTrace().trace(
302 GitTraceLocation.UI.getLocation(),
303 "Phantom file, so removing from index"); //$NON-NLS-1$
304 continue;
305 } else {
306 if (idxEntry.update(thisfile))
307 index.write();
310 if (notTracked.contains(file)) {
311 idxEntry = index.add(repositoryMapping.getWorkDir(), new File(repositoryMapping.getWorkDir(),
312 repoRelativePath));
313 index.write();
318 if (idxEntry != null) {
319 projTree.addFile(repoRelativePath);
320 TreeEntry newMember = projTree.findBlobMember(repoRelativePath);
322 newMember.setId(idxEntry.getObjectId());
323 // TODO is this the right Location?
324 if (GitTraceLocation.UI.isActive())
325 GitTraceLocation.getTrace().trace(
326 GitTraceLocation.UI.getLocation(),
327 "New member id for " + repoRelativePath //$NON-NLS-1$
328 + ": " + newMember.getId() + " idx id: " //$NON-NLS-1$ //$NON-NLS-2$
329 + idxEntry.getObjectId());
334 private String buildReflogMessage(String commitMessage) {
335 String firstLine = commitMessage;
336 int newlineIndex = commitMessage.indexOf("\n"); //$NON-NLS-1$
337 if (newlineIndex > 0) {
338 firstLine = commitMessage.substring(0, newlineIndex);
340 String commitStr = amending ? "commit (amend):" : "commit: "; //$NON-NLS-1$ //$NON-NLS-2$
341 String message = commitStr + firstLine;
342 return message;
345 private void writeTreeWithSubTrees(Tree tree) throws TeamException {
346 if (tree.getId() == null) {
347 // TODO is this the right Location?
348 if (GitTraceLocation.UI.isActive())
349 GitTraceLocation.getTrace().trace(
350 GitTraceLocation.UI.getLocation(),
351 "writing tree for: " + tree.getFullName()); //$NON-NLS-1$
352 try {
353 for (TreeEntry entry : tree.members()) {
354 if (entry.isModified()) {
355 if (entry instanceof Tree) {
356 writeTreeWithSubTrees((Tree) entry);
357 } else {
358 // this shouldn't happen.... not quite sure what to
359 // do here :)
360 // TODO is this the right Location?
361 if (GitTraceLocation.UI.isActive())
362 GitTraceLocation.getTrace().trace(
363 GitTraceLocation.UI.getLocation(),
364 "BAD JUJU: " //$NON-NLS-1$
365 + entry.getFullName());
369 ObjectWriter writer = new ObjectWriter(tree.getRepository());
370 tree.setId(writer.writeTree(tree));
371 } catch (IOException e) {
372 throw new TeamException(UIText.CommitAction_errorWritingTrees, e);
377 private void buildIndexHeadDiffList() throws IOException, CoreException {
378 HashMap<Repository, HashSet<IProject>> repositories = new HashMap<Repository, HashSet<IProject>>();
380 for (IProject project : getProjectsInRepositoryOfSelectedResources()) {
381 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
382 assert repositoryMapping != null;
384 Repository repository = repositoryMapping.getRepository();
386 HashSet<IProject> projects = repositories.get(repository);
388 if (projects == null) {
389 projects = new HashSet<IProject>();
390 repositories.put(repository, projects);
393 projects.add(project);
396 for (Map.Entry<Repository, HashSet<IProject>> entry : repositories.entrySet()) {
397 Repository repository = entry.getKey();
398 HashSet<IProject> projects = entry.getValue();
400 Tree head = repository.mapTree(Constants.HEAD);
401 GitIndex index = repository.getIndex();
402 IndexDiff indexDiff = new IndexDiff(head, index);
403 indexDiff.diff();
405 for (IProject project : projects) {
406 includeList(project, indexDiff.getAdded(), indexChanges);
407 includeList(project, indexDiff.getChanged(), indexChanges);
408 includeList(project, indexDiff.getRemoved(), indexChanges);
409 includeList(project, indexDiff.getMissing(), notIndexed);
410 includeList(project, indexDiff.getModified(), notIndexed);
411 addUntrackedFiles(repository, project);
416 private void addUntrackedFiles(final Repository repository, final IProject project) throws CoreException, IOException {
417 final GitIndex index = repository.getIndex();
418 final Tree headTree = repository.mapTree(Constants.HEAD);
419 project.accept(new IResourceVisitor() {
421 public boolean visit(IResource resource) throws CoreException {
422 if (Team.isIgnoredHint(resource))
423 return false;
424 if (resource.getType() == IResource.FILE) {
426 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(resource);
427 try {
428 TreeEntry headEntry = (headTree == null ? null : headTree.findBlobMember(repoRelativePath));
429 if (headEntry == null){
430 Entry indexEntry = null;
431 indexEntry = index.getEntry(repoRelativePath);
433 if (indexEntry == null) {
434 notTracked.add((IFile)resource);
435 files.add((IFile)resource);
438 } catch (IOException e) {
439 throw new TeamException(UIText.CommitAction_InternalError, e);
442 return true;
449 private void includeList(IProject project, HashSet<String> added, ArrayList<IFile> category) {
450 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(project);
451 if (repoRelativePath.length() > 0) {
452 repoRelativePath += "/"; //$NON-NLS-1$
455 for (String filename : added) {
456 try {
457 if (!filename.startsWith(repoRelativePath))
458 continue;
459 String projectRelativePath = filename.substring(repoRelativePath.length());
460 IResource member = project.getFile(projectRelativePath);
461 if (member != null && member instanceof IFile) {
462 if (!files.contains(member))
463 files.add((IFile) member);
464 category.add((IFile) member);
465 } else {
466 // TODO is this the right Location?
467 if (GitTraceLocation.UI.isActive())
468 GitTraceLocation.getTrace().trace(
469 GitTraceLocation.UI.getLocation(),
470 "Couldn't find " + filename); //$NON-NLS-1$
472 } catch (Exception e) {
473 if (GitTraceLocation.UI.isActive())
474 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
475 continue;
476 } // if it's outside the workspace, bad things happen
480 boolean tryAddResource(IFile resource, GitProjectData projectData, ArrayList<IFile> category) {
481 if (files.contains(resource))
482 return false;
484 try {
485 RepositoryMapping repositoryMapping = projectData
486 .getRepositoryMapping(resource);
488 if (isChanged(repositoryMapping, resource)) {
489 files.add(resource);
490 category.add(resource);
491 return true;
493 } catch (Exception e) {
494 if (GitTraceLocation.UI.isActive())
495 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
497 return false;
500 private boolean isChanged(RepositoryMapping map, IFile resource) {
501 try {
502 Repository repository = map.getRepository();
503 GitIndex index = repository.getIndex();
504 String repoRelativePath = map.getRepoRelativePath(resource);
505 Entry entry = index.getEntry(repoRelativePath);
506 if (entry != null)
507 return entry.isModified(map.getWorkDir());
508 return false;
509 } catch (UnsupportedEncodingException e) {
510 if (GitTraceLocation.UI.isActive())
511 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
512 } catch (IOException e) {
513 if (GitTraceLocation.UI.isActive())
514 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
516 return false;
519 @Override
520 public boolean isEnabled() {
521 return getProjectsInRepositoryOfSelectedResources().length > 0;