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
.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
;
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
;
72 public void execute(IAction act
) {
75 buildIndexHeadDiffList();
76 } catch (IOException e
) {
78 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
79 e
), UIText
.CommitAction_errorDuringCommit
,
80 UIText
.CommitAction_errorComputingDiffs
);
82 } catch (CoreException e
) {
84 new TeamException(UIText
.CommitAction_errorComputingDiffs
,
85 e
), UIText
.CommitAction_errorDuringCommit
,
86 UIText
.CommitAction_errorComputingDiffs
);
90 Repository
[] repos
= getRepositoriesFor(getProjectsForSelectedResources());
91 Repository repository
= null;
92 amendAllowed
= repos
.length
== 1;
93 for (Repository repo
: repos
) {
95 if (!repo
.getRepositoryState().canCommit()) {
96 MessageDialog
.openError(getTargetPart().getSite().getShell(),
97 UIText
.CommitAction_cannotCommit
,
98 NLS
.bind(UIText
.CommitAction_repositoryState
, repo
.getRepositoryState().getDescription()));
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
);
114 MessageDialog
.openWarning(getTargetPart().getSite().getShell(), UIText
.CommitAction_noFilesToCommit
, UIText
.CommitAction_amendNotPossible
);
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
)
150 String commitMessage
= commitDialog
.getCommitMessage();
151 amending
= commitDialog
.isAmending();
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
>();
166 previousCommit
= null;
169 private void loadPreviousCommit() {
170 IProject project
= getProjectsForSelectedResources()[0];
172 Repository repo
= RepositoryMapping
.getMapping(project
).getRepository();
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
>();
189 prepareTrees(selectedItems
, treeMap
);
190 } catch (IOException e
) {
191 throw new TeamException(UIText
.CommitAction_errorPreparingTrees
, e
);
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
;
223 parentIds
= previousCommit
.getParentIds();
225 if (currentHeadId
!= null)
226 parentIds
= new ObjectId
[] { currentHeadId
};
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)
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
);
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$
303 if (idxEntry
.update(thisfile
))
307 if (notTracked
.contains(file
)) {
308 idxEntry
= index
.add(repositoryMapping
.getWorkDir(), new File(repositoryMapping
.getWorkDir(),
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
;
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$
350 for (TreeEntry entry
: tree
.members()) {
351 if (entry
.isModified()) {
352 if (entry
instanceof Tree
) {
353 writeTreeWithSubTrees((Tree
) entry
);
355 // this shouldn't happen.... not quite sure what to
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
);
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
))
421 if (resource
.getType() == IResource
.FILE
) {
423 String repoRelativePath
= RepositoryMapping
.getMapping(project
).getRepoRelativePath(resource
);
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
);
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
) {
454 if (!filename
.startsWith(repoRelativePath
))
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
);
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
);
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
))
482 RepositoryMapping repositoryMapping
= projectData
483 .getRepositoryMapping(resource
);
485 if (isChanged(repositoryMapping
, resource
)) {
487 category
.add(resource
);
490 } catch (Exception e
) {
491 if (GitTraceLocation
.UI
.isActive())
492 GitTraceLocation
.getTrace().trace(GitTraceLocation
.UI
.getLocation(), e
.getMessage(), e
);
497 private boolean isChanged(RepositoryMapping map
, IFile resource
) {
499 Repository repository
= map
.getRepository();
500 GitIndex index
= repository
.getIndex();
501 String repoRelativePath
= map
.getRepoRelativePath(resource
);
502 Entry entry
= index
.getEntry(repoRelativePath
);
504 return entry
.isModified(map
.getWorkDir());
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
);
517 public boolean isEnabled() {
518 return getProjectsInRepositoryOfSelectedResources().length
> 0;