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
;
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
;
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
;
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
;
75 public void execute(IAction act
) {
78 buildIndexHeadDiffList();
79 } catch (IOException e
) {
81 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
82 e
), UIText
.CommitAction_errorDuringCommit
,
83 UIText
.CommitAction_errorComputingDiffs
);
85 } catch (CoreException e
) {
87 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
88 e
), UIText
.CommitAction_errorDuringCommit
,
89 UIText
.CommitAction_errorComputingDiffs
);
93 Repository
[] repos
= getRepositoriesFor(getProjectsForSelectedResources());
94 Repository repository
= null;
95 amendAllowed
= repos
.length
== 1;
96 for (Repository repo
: repos
) {
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()));
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
);
119 MessageDialog
.openWarning(getTargetPart().getSite().getShell(), UIText
.CommitAction_noFilesToCommit
, UIText
.CommitAction_amendNotPossible
);
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
)
156 String commitMessage
= commitDialog
.getCommitMessage();
157 amending
= commitDialog
.isAmending();
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
>();
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
);
199 return preselectionCandidates
;
202 private void loadPreviousCommit() {
203 IProject project
= getProjectsForSelectedResources()[0];
205 Repository repo
= RepositoryMapping
.getMapping(project
).getRepository();
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
>();
222 prepareTrees(selectedItems
, treeMap
);
223 } catch (IOException e
) {
224 throw new TeamException(UIText
.CommitAction_errorPreparingTrees
, e
);
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
;
256 parentIds
= previousCommit
.getParentIds();
258 if (currentHeadId
!= null)
259 parentIds
= new ObjectId
[] { currentHeadId
};
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)
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
);
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$
336 if (idxEntry
.update(thisfile
))
340 if (notTracked
.contains(file
)) {
341 idxEntry
= index
.add(repositoryMapping
.getWorkDir(), new File(repositoryMapping
.getWorkDir(),
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
;
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$
383 for (TreeEntry entry
: tree
.members()) {
384 if (entry
.isModified()) {
385 if (entry
instanceof Tree
) {
386 writeTreeWithSubTrees((Tree
) entry
);
388 // this shouldn't happen.... not quite sure what to
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
);
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
))
454 if (resource
.getType() == IResource
.FILE
) {
456 String repoRelativePath
= RepositoryMapping
.getMapping(project
).getRepoRelativePath(resource
);
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
);
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
) {
487 if (!filename
.startsWith(repoRelativePath
))
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
);
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
);
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
))
515 RepositoryMapping repositoryMapping
= projectData
516 .getRepositoryMapping(resource
);
518 if (isChanged(repositoryMapping
, resource
)) {
520 category
.add(resource
);
523 } catch (Exception e
) {
524 if (GitTraceLocation
.UI
.isActive())
525 GitTraceLocation
.getTrace().trace(GitTraceLocation
.UI
.getLocation(), e
.getMessage(), e
);
530 private boolean isChanged(RepositoryMapping map
, IFile resource
) {
532 Repository repository
= map
.getRepository();
533 GitIndex index
= repository
.getIndex();
534 String repoRelativePath
= map
.getRepoRelativePath(resource
);
535 Entry entry
= index
.getEntry(repoRelativePath
);
537 return entry
.isModified(map
.getWorkDir());
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
);
550 public boolean isEnabled() {
551 return getProjectsInRepositoryOfSelectedResources().length
> 0;