Git Repositories View: Simple fetch and push
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / actions / CommitAction.java
blob3ee292c696f5e382e868ceed89dd72035beea717
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.Collection;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.TimeZone;
27 import org.eclipse.core.resources.IFile;
28 import org.eclipse.core.resources.IProject;
29 import org.eclipse.core.resources.IResource;
30 import org.eclipse.core.resources.IResourceVisitor;
31 import org.eclipse.core.runtime.CoreException;
32 import org.eclipse.egit.core.project.GitProjectData;
33 import org.eclipse.egit.core.project.RepositoryMapping;
34 import org.eclipse.egit.ui.UIText;
35 import org.eclipse.egit.ui.internal.dialogs.CommitDialog;
36 import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
37 import org.eclipse.jface.action.IAction;
38 import org.eclipse.jface.dialogs.IDialogConstants;
39 import org.eclipse.jface.dialogs.MessageDialog;
40 import org.eclipse.jgit.lib.Commit;
41 import org.eclipse.jgit.lib.Constants;
42 import org.eclipse.jgit.lib.GitIndex;
43 import org.eclipse.jgit.lib.IndexDiff;
44 import org.eclipse.jgit.lib.ObjectId;
45 import org.eclipse.jgit.lib.ObjectWriter;
46 import org.eclipse.jgit.lib.PersonIdent;
47 import org.eclipse.jgit.lib.RefUpdate;
48 import org.eclipse.jgit.lib.Repository;
49 import org.eclipse.jgit.lib.RepositoryConfig;
50 import org.eclipse.jgit.lib.RepositoryState;
51 import org.eclipse.jgit.lib.Tree;
52 import org.eclipse.jgit.lib.TreeEntry;
53 import org.eclipse.jgit.lib.GitIndex.Entry;
54 import org.eclipse.osgi.util.NLS;
55 import org.eclipse.team.core.Team;
56 import org.eclipse.team.core.TeamException;
57 import org.eclipse.team.internal.ui.Utils;
59 /**
60 * Scan for modified resources in the same project as the selected resources.
62 public class CommitAction extends RepositoryAction {
64 private ArrayList<IFile> notIndexed;
65 private ArrayList<IFile> indexChanges;
66 private ArrayList<IFile> notTracked;
67 private ArrayList<IFile> files;
69 private Commit previousCommit;
71 private boolean amendAllowed;
72 private boolean amending;
74 @Override
75 public void execute(IAction act) {
76 resetState();
77 try {
78 buildIndexHeadDiffList();
79 } catch (IOException e) {
80 handle(
81 new TeamException(UIText.CommitAction_errorComputingDiffs,
82 e), UIText.CommitAction_errorDuringCommit,
83 UIText.CommitAction_errorComputingDiffs);
84 return;
85 } catch (CoreException e) {
86 handle(
87 new TeamException(UIText.CommitAction_errorComputingDiffs,
88 e), UIText.CommitAction_errorDuringCommit,
89 UIText.CommitAction_errorComputingDiffs);
90 return;
93 Repository[] repos = getRepositoriesFor(getProjectsForSelectedResources());
94 Repository repository = null;
95 amendAllowed = repos.length == 1;
96 for (Repository repo : repos) {
97 repository = repo;
98 RepositoryState state = repo.getRepositoryState();
99 // currently we don't support committing a merge commit
100 if (state == RepositoryState.MERGING_RESOLVED || !state.canCommit()) {
101 MessageDialog.openError(getTargetPart().getSite().getShell(),
102 UIText.CommitAction_cannotCommit,
103 NLS.bind(UIText.CommitAction_repositoryState, state.getDescription()));
104 return;
108 loadPreviousCommit();
109 if (files.isEmpty()) {
110 if (amendAllowed && previousCommit != null) {
111 boolean result = MessageDialog
112 .openQuestion(getTargetPart().getSite().getShell(),
113 UIText.CommitAction_noFilesToCommit,
114 UIText.CommitAction_amendCommit);
115 if (!result)
116 return;
117 amending = true;
118 } else {
119 MessageDialog.openWarning(getTargetPart().getSite().getShell(), UIText.CommitAction_noFilesToCommit, UIText.CommitAction_amendNotPossible);
120 return;
124 String author = null;
125 String committer = null;
126 if (repository != null) {
127 final RepositoryConfig config = repository.getConfig();
128 author = config.getAuthorName();
129 final String authorEmail = config.getAuthorEmail();
130 author = author + " <" + authorEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
132 committer = config.getCommitterName();
133 final String committerEmail = config.getCommitterEmail();
134 committer = committer + " <" + committerEmail + ">"; //$NON-NLS-1$ //$NON-NLS-2$
137 CommitDialog commitDialog = new CommitDialog(getTargetPart().getSite().getShell());
138 commitDialog.setAmending(amending);
139 commitDialog.setAmendAllowed(amendAllowed);
140 commitDialog.setFileList(files);
141 commitDialog.setPreselectedFiles(getSelectedFiles());
142 commitDialog.setAuthor(author);
143 commitDialog.setCommitter(committer);
144 if(notTracked.size() == files.size())
145 commitDialog.setShowUntracked(true);
147 if (previousCommit != null) {
148 commitDialog.setPreviousCommitMessage(previousCommit.getMessage());
149 PersonIdent previousAuthor = previousCommit.getAuthor();
150 commitDialog.setPreviousAuthor(previousAuthor.getName() + " <" + previousAuthor.getEmailAddress() + ">"); //$NON-NLS-1$ //$NON-NLS-2$
153 if (commitDialog.open() != IDialogConstants.OK_ID)
154 return;
156 String commitMessage = commitDialog.getCommitMessage();
157 amending = commitDialog.isAmending();
158 try {
159 performCommit(commitDialog, commitMessage);
160 } catch (TeamException e) {
161 handle(e, UIText.CommitAction_errorDuringCommit,
162 UIText.CommitAction_errorOnCommit);
166 private void resetState() {
167 files = new ArrayList<IFile>();
168 notIndexed = new ArrayList<IFile>();
169 indexChanges = new ArrayList<IFile>();
170 notTracked = new ArrayList<IFile>();
171 amending = false;
172 previousCommit = null;
176 * Retrieves a collection of files that may be committed based on the user's
177 * selection when they performed the commit action. That is, even if the
178 * user only selected one folder when the action was performed, if the
179 * folder contains any files that could be committed, they will be returned.
181 * @return a collection of files that is eligible to be committed based on
182 * the user's selection
184 private Collection<IFile> getSelectedFiles() {
185 List<IFile> preselectionCandidates = new ArrayList<IFile>();
186 // get the resources the user selected
187 IResource[] selectedResources = getSelectedResources();
188 // iterate through all the files that may be committed
189 for (IFile file : files) {
190 for (IResource resource : selectedResources) {
191 // if any selected resource contains the file, add it as a
192 // preselection candidate
193 if (resource.contains(file)) {
194 preselectionCandidates.add(file);
195 break;
199 return preselectionCandidates;
202 private void loadPreviousCommit() {
203 IProject project = getProjectsForSelectedResources()[0];
205 Repository repo = RepositoryMapping.getMapping(project).getRepository();
206 try {
207 ObjectId parentId = repo.resolve(Constants.HEAD);
208 if (parentId != null)
209 previousCommit = repo.mapCommit(parentId);
210 } catch (IOException e) {
211 Utils.handleError(getTargetPart().getSite().getShell(), e, UIText.CommitAction_errorDuringCommit, UIText.CommitAction_errorRetrievingCommit);
215 private void performCommit(CommitDialog commitDialog, String commitMessage)
216 throws TeamException {
218 IFile[] selectedItems = commitDialog.getSelectedFiles();
220 HashMap<Repository, Tree> treeMap = new HashMap<Repository, Tree>();
221 try {
222 prepareTrees(selectedItems, treeMap);
223 } catch (IOException e) {
224 throw new TeamException(UIText.CommitAction_errorPreparingTrees, e);
227 try {
228 doCommits(commitDialog, commitMessage, treeMap);
229 } catch (IOException e) {
230 throw new TeamException(UIText.CommitAction_errorCommittingChanges, e);
232 for (IProject proj : getProjectsForSelectedResources()) {
233 RepositoryMapping.getMapping(proj).fireRepositoryChanged();
237 private void doCommits(CommitDialog commitDialog, String commitMessage,
238 HashMap<Repository, Tree> treeMap) throws IOException, TeamException {
240 final String author = commitDialog.getAuthor();
241 final String committer = commitDialog.getCommitter();
242 final Date commitDate = new Date();
243 final TimeZone timeZone = TimeZone.getDefault();
245 final PersonIdent authorIdent = new PersonIdent(author);
246 final PersonIdent committerIdent = new PersonIdent(committer);
248 for (java.util.Map.Entry<Repository, Tree> entry : treeMap.entrySet()) {
249 Tree tree = entry.getValue();
250 Repository repo = tree.getRepository();
251 writeTreeWithSubTrees(tree);
253 ObjectId currentHeadId = repo.resolve(Constants.HEAD);
254 ObjectId[] parentIds;
255 if (amending) {
256 parentIds = previousCommit.getParentIds();
257 } else {
258 if (currentHeadId != null)
259 parentIds = new ObjectId[] { currentHeadId };
260 else
261 parentIds = new ObjectId[0];
263 Commit commit = new Commit(repo, parentIds);
264 commit.setTree(tree);
265 commit.setMessage(commitMessage);
266 commit.setAuthor(new PersonIdent(authorIdent, commitDate, timeZone));
267 commit.setCommitter(new PersonIdent(committerIdent, commitDate, timeZone));
269 ObjectWriter writer = new ObjectWriter(repo);
270 commit.setCommitId(writer.writeCommit(commit));
272 final RefUpdate ru = repo.updateRef(Constants.HEAD);
273 ru.setNewObjectId(commit.getCommitId());
274 ru.setRefLogMessage(buildReflogMessage(commitMessage), false);
275 if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE) {
276 throw new TeamException(
277 NLS.bind(UIText.CommitAction_failedToUpdate, ru.getName(),
278 commit.getCommitId()));
283 private void prepareTrees(IFile[] selectedItems,
284 HashMap<Repository, Tree> treeMap) throws IOException,
285 UnsupportedEncodingException {
286 if (selectedItems.length == 0) {
287 // amending commit - need to put something into the map
288 for (IProject proj : getProjectsForSelectedResources()) {
289 Repository repo = RepositoryMapping.getMapping(proj).getRepository();
290 if (!treeMap.containsKey(repo))
291 treeMap.put(repo, repo.mapTree(Constants.HEAD));
295 for (IFile file : selectedItems) {
297 IProject project = file.getProject();
298 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
299 Repository repository = repositoryMapping.getRepository();
300 Tree projTree = treeMap.get(repository);
301 if (projTree == null) {
302 projTree = repository.mapTree(Constants.HEAD);
303 if (projTree == null)
304 projTree = new Tree(repository);
305 treeMap.put(repository, projTree);
306 // TODO is this the right Location?
307 if (GitTraceLocation.UI.isActive())
308 GitTraceLocation.getTrace().trace(
309 GitTraceLocation.UI.getLocation(),
310 "Orig tree id: " + projTree.getId()); //$NON-NLS-1$
312 GitIndex index = repository.getIndex();
313 String repoRelativePath = repositoryMapping
314 .getRepoRelativePath(file);
315 String string = repoRelativePath;
317 TreeEntry treeMember = projTree.findBlobMember(repoRelativePath);
318 // we always want to delete it from the current tree, since if it's
319 // updated, we'll add it again
320 if (treeMember != null)
321 treeMember.delete();
323 Entry idxEntry = index.getEntry(string);
324 if (notIndexed.contains(file)) {
325 File thisfile = new File(repositoryMapping.getWorkDir(), idxEntry.getName());
326 if (!thisfile.isFile()) {
327 index.remove(repositoryMapping.getWorkDir(), thisfile);
328 index.write();
329 // TODO is this the right Location?
330 if (GitTraceLocation.UI.isActive())
331 GitTraceLocation.getTrace().trace(
332 GitTraceLocation.UI.getLocation(),
333 "Phantom file, so removing from index"); //$NON-NLS-1$
334 continue;
335 } else {
336 if (idxEntry.update(thisfile))
337 index.write();
340 if (notTracked.contains(file)) {
341 idxEntry = index.add(repositoryMapping.getWorkDir(), new File(repositoryMapping.getWorkDir(),
342 repoRelativePath));
343 index.write();
348 if (idxEntry != null) {
349 projTree.addFile(repoRelativePath);
350 TreeEntry newMember = projTree.findBlobMember(repoRelativePath);
352 newMember.setId(idxEntry.getObjectId());
353 // TODO is this the right Location?
354 if (GitTraceLocation.UI.isActive())
355 GitTraceLocation.getTrace().trace(
356 GitTraceLocation.UI.getLocation(),
357 "New member id for " + repoRelativePath //$NON-NLS-1$
358 + ": " + newMember.getId() + " idx id: " //$NON-NLS-1$ //$NON-NLS-2$
359 + idxEntry.getObjectId());
364 private String buildReflogMessage(String commitMessage) {
365 String firstLine = commitMessage;
366 int newlineIndex = commitMessage.indexOf("\n"); //$NON-NLS-1$
367 if (newlineIndex > 0) {
368 firstLine = commitMessage.substring(0, newlineIndex);
370 String commitStr = amending ? "commit (amend):" : "commit: "; //$NON-NLS-1$ //$NON-NLS-2$
371 String message = commitStr + firstLine;
372 return message;
375 private void writeTreeWithSubTrees(Tree tree) throws TeamException {
376 if (tree.getId() == null) {
377 // TODO is this the right Location?
378 if (GitTraceLocation.UI.isActive())
379 GitTraceLocation.getTrace().trace(
380 GitTraceLocation.UI.getLocation(),
381 "writing tree for: " + tree.getFullName()); //$NON-NLS-1$
382 try {
383 for (TreeEntry entry : tree.members()) {
384 if (entry.isModified()) {
385 if (entry instanceof Tree) {
386 writeTreeWithSubTrees((Tree) entry);
387 } else {
388 // this shouldn't happen.... not quite sure what to
389 // do here :)
390 // TODO is this the right Location?
391 if (GitTraceLocation.UI.isActive())
392 GitTraceLocation.getTrace().trace(
393 GitTraceLocation.UI.getLocation(),
394 "BAD JUJU: " //$NON-NLS-1$
395 + entry.getFullName());
399 ObjectWriter writer = new ObjectWriter(tree.getRepository());
400 tree.setId(writer.writeTree(tree));
401 } catch (IOException e) {
402 throw new TeamException(UIText.CommitAction_errorWritingTrees, e);
407 private void buildIndexHeadDiffList() throws IOException, CoreException {
408 HashMap<Repository, HashSet<IProject>> repositories = new HashMap<Repository, HashSet<IProject>>();
410 for (IProject project : getProjectsInRepositoryOfSelectedResources()) {
411 RepositoryMapping repositoryMapping = RepositoryMapping.getMapping(project);
412 assert repositoryMapping != null;
414 Repository repository = repositoryMapping.getRepository();
416 HashSet<IProject> projects = repositories.get(repository);
418 if (projects == null) {
419 projects = new HashSet<IProject>();
420 repositories.put(repository, projects);
423 projects.add(project);
426 for (Map.Entry<Repository, HashSet<IProject>> entry : repositories.entrySet()) {
427 Repository repository = entry.getKey();
428 HashSet<IProject> projects = entry.getValue();
430 Tree head = repository.mapTree(Constants.HEAD);
431 GitIndex index = repository.getIndex();
432 IndexDiff indexDiff = new IndexDiff(head, index);
433 indexDiff.diff();
435 for (IProject project : projects) {
436 includeList(project, indexDiff.getAdded(), indexChanges);
437 includeList(project, indexDiff.getChanged(), indexChanges);
438 includeList(project, indexDiff.getRemoved(), indexChanges);
439 includeList(project, indexDiff.getMissing(), notIndexed);
440 includeList(project, indexDiff.getModified(), notIndexed);
441 addUntrackedFiles(repository, project);
446 private void addUntrackedFiles(final Repository repository, final IProject project) throws CoreException, IOException {
447 final GitIndex index = repository.getIndex();
448 final Tree headTree = repository.mapTree(Constants.HEAD);
449 project.accept(new IResourceVisitor() {
451 public boolean visit(IResource resource) throws CoreException {
452 if (Team.isIgnoredHint(resource))
453 return false;
454 if (resource.getType() == IResource.FILE) {
456 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(resource);
457 try {
458 TreeEntry headEntry = (headTree == null ? null : headTree.findBlobMember(repoRelativePath));
459 if (headEntry == null){
460 Entry indexEntry = null;
461 indexEntry = index.getEntry(repoRelativePath);
463 if (indexEntry == null) {
464 notTracked.add((IFile)resource);
465 files.add((IFile)resource);
468 } catch (IOException e) {
469 throw new TeamException(UIText.CommitAction_InternalError, e);
472 return true;
479 private void includeList(IProject project, HashSet<String> added, ArrayList<IFile> category) {
480 String repoRelativePath = RepositoryMapping.getMapping(project).getRepoRelativePath(project);
481 if (repoRelativePath.length() > 0) {
482 repoRelativePath += "/"; //$NON-NLS-1$
485 for (String filename : added) {
486 try {
487 if (!filename.startsWith(repoRelativePath))
488 continue;
489 String projectRelativePath = filename.substring(repoRelativePath.length());
490 IResource member = project.getFile(projectRelativePath);
491 if (member != null && member instanceof IFile) {
492 if (!files.contains(member))
493 files.add((IFile) member);
494 category.add((IFile) member);
495 } else {
496 // TODO is this the right Location?
497 if (GitTraceLocation.UI.isActive())
498 GitTraceLocation.getTrace().trace(
499 GitTraceLocation.UI.getLocation(),
500 "Couldn't find " + filename); //$NON-NLS-1$
502 } catch (Exception e) {
503 if (GitTraceLocation.UI.isActive())
504 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
505 continue;
506 } // if it's outside the workspace, bad things happen
510 boolean tryAddResource(IFile resource, GitProjectData projectData, ArrayList<IFile> category) {
511 if (files.contains(resource))
512 return false;
514 try {
515 RepositoryMapping repositoryMapping = projectData
516 .getRepositoryMapping(resource);
518 if (isChanged(repositoryMapping, resource)) {
519 files.add(resource);
520 category.add(resource);
521 return true;
523 } catch (Exception e) {
524 if (GitTraceLocation.UI.isActive())
525 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
527 return false;
530 private boolean isChanged(RepositoryMapping map, IFile resource) {
531 try {
532 Repository repository = map.getRepository();
533 GitIndex index = repository.getIndex();
534 String repoRelativePath = map.getRepoRelativePath(resource);
535 Entry entry = index.getEntry(repoRelativePath);
536 if (entry != null)
537 return entry.isModified(map.getWorkDir());
538 return false;
539 } catch (UnsupportedEncodingException e) {
540 if (GitTraceLocation.UI.isActive())
541 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
542 } catch (IOException e) {
543 if (GitTraceLocation.UI.isActive())
544 GitTraceLocation.getTrace().trace(GitTraceLocation.UI.getLocation(), e.getMessage(), e);
546 return false;
549 @Override
550 public boolean isEnabled() {
551 return getProjectsInRepositoryOfSelectedResources().length > 0;