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
.Date
;
20 import java
.util
.HashMap
;
21 import java
.util
.HashSet
;
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
;
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
;
73 public void execute(IAction act
) {
76 buildIndexHeadDiffList();
77 } catch (IOException e
) {
79 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
80 e
), UIText
.CommitAction_errorDuringCommit
,
81 UIText
.CommitAction_errorComputingDiffs
);
83 } catch (CoreException e
) {
85 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
86 e
), UIText
.CommitAction_errorDuringCommit
,
87 UIText
.CommitAction_errorComputingDiffs
);
91 Repository
[] repos
= getRepositoriesFor(getProjectsForSelectedResources());
92 Repository repository
= null;
93 amendAllowed
= repos
.length
== 1;
94 for (Repository repo
: repos
) {
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()));
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
);
117 MessageDialog
.openWarning(getTargetPart().getSite().getShell(), UIText
.CommitAction_noFilesToCommit
, UIText
.CommitAction_amendNotPossible
);
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
)
153 String commitMessage
= commitDialog
.getCommitMessage();
154 amending
= commitDialog
.isAmending();
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
>();
169 previousCommit
= null;
172 private void loadPreviousCommit() {
173 IProject project
= getProjectsForSelectedResources()[0];
175 Repository repo
= RepositoryMapping
.getMapping(project
).getRepository();
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
>();
192 prepareTrees(selectedItems
, treeMap
);
193 } catch (IOException e
) {
194 throw new TeamException(UIText
.CommitAction_errorPreparingTrees
, e
);
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
;
226 parentIds
= previousCommit
.getParentIds();
228 if (currentHeadId
!= null)
229 parentIds
= new ObjectId
[] { currentHeadId
};
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)
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
);
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$
306 if (idxEntry
.update(thisfile
))
310 if (notTracked
.contains(file
)) {
311 idxEntry
= index
.add(repositoryMapping
.getWorkDir(), new File(repositoryMapping
.getWorkDir(),
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
;
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$
353 for (TreeEntry entry
: tree
.members()) {
354 if (entry
.isModified()) {
355 if (entry
instanceof Tree
) {
356 writeTreeWithSubTrees((Tree
) entry
);
358 // this shouldn't happen.... not quite sure what to
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
);
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
))
424 if (resource
.getType() == IResource
.FILE
) {
426 String repoRelativePath
= RepositoryMapping
.getMapping(project
).getRepoRelativePath(resource
);
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
);
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
) {
457 if (!filename
.startsWith(repoRelativePath
))
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
);
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
);
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
))
485 RepositoryMapping repositoryMapping
= projectData
486 .getRepositoryMapping(resource
);
488 if (isChanged(repositoryMapping
, resource
)) {
490 category
.add(resource
);
493 } catch (Exception e
) {
494 if (GitTraceLocation
.UI
.isActive())
495 GitTraceLocation
.getTrace().trace(GitTraceLocation
.UI
.getLocation(), e
.getMessage(), e
);
500 private boolean isChanged(RepositoryMapping map
, IFile resource
) {
502 Repository repository
= map
.getRepository();
503 GitIndex index
= repository
.getIndex();
504 String repoRelativePath
= map
.getRepoRelativePath(resource
);
505 Entry entry
= index
.getEntry(repoRelativePath
);
507 return entry
.isModified(map
.getWorkDir());
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
);
520 public boolean isEnabled() {
521 return getProjectsInRepositoryOfSelectedResources().length
> 0;