2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common
8 * Development and Distribution License("CDDL") (collectively, the
9 * "License"). You may not use this file except in compliance with the
10 * License. You can obtain a copy of the License at
11 * http://www.netbeans.org/cddl-gplv2.html
12 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13 * specific language governing permissions and limitations under the
14 * License. When distributing the software, include this License Header
15 * Notice in each file and include the License file at
16 * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17 * particular file as subject to the "Classpath" exception as provided
18 * by Sun in the GPL Version 2 section of the License file that
19 * accompanied this code. If applicable, add the following below the
20 * License Header, with the fields enclosed by brackets [] replaced by
21 * your own identifying information:
22 * "Portions Copyrighted [year] [name of copyright owner]"
26 * The Original Software is NetBeans. The Initial Developer of the Original
27 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28 * Microsystems, Inc. All Rights Reserved.
29 * Portions Copyright 2008 Alexander Coles (Ikonoklastik Productions).
31 * If you wish your version of this file to be governed by only the CDDL
32 * or only the GPL Version 2, indicate your decision by adding
33 * "[Contributor] elects to include this software in this distribution
34 * under the [CDDL or GPL Version 2] license." If you do not indicate a
35 * single choice of license, a recipient has the option to distribute
36 * your version of this file under either the CDDL, the GPL Version 2 or
37 * to extend the choice of license to its licensees as provided above.
38 * However, if you add GPL Version 2 code and therefore, elected the GPL
39 * Version 2 license, then the option applies only if the new code is
40 * made subject to such option by the copyright holder.
44 import org
.nbgit
.util
.GitUtils
;
45 import java
.awt
.Image
;
47 import java
.util
.ArrayList
;
48 import java
.util
.HashMap
;
49 import java
.util
.List
;
51 import java
.util
.ResourceBundle
;
52 import java
.util
.concurrent
.ConcurrentLinkedQueue
;
53 import java
.util
.logging
.Level
;
54 import javax
.swing
.Action
;
55 import org
.nbgit
.ui
.browser
.BrowserAction
;
56 import org
.nbgit
.ui
.clone
.CloneAction
;
57 import org
.nbgit
.ui
.clone
.CloneExternalAction
;
58 import org
.nbgit
.ui
.commit
.CommitAction
;
59 import org
.nbgit
.ui
.custom
.CustomMenu
;
60 import org
.nbgit
.ui
.diff
.DiffAction
;
61 import org
.nbgit
.ui
.init
.InitAction
;
62 import org
.nbgit
.ui
.log
.LogAction
;
63 import org
.nbgit
.ui
.properties
.PropertiesAction
;
64 import org
.nbgit
.ui
.status
.StatusAction
;
65 import org
.nbgit
.ui
.update
.RevertModificationsAction
;
66 import org
.nbgit
.ui
.update
.UpdateAction
;
67 import org
.nbgit
.util
.HtmlFormatter
;
68 import org
.netbeans
.api
.project
.Project
;
69 import org
.netbeans
.modules
.versioning
.spi
.VCSAnnotator
;
70 import org
.netbeans
.modules
.versioning
.spi
.VCSContext
;
71 import org
.netbeans
.modules
.versioning
.spi
.VersioningSupport
;
72 import org
.netbeans
.modules
.versioning
.util
.Utils
;
73 import org
.openide
.nodes
.Node
;
74 import org
.openide
.util
.ImageUtilities
;
75 import org
.openide
.util
.NbBundle
;
76 import org
.openide
.util
.RequestProcessor
;
79 * Responsible for coloring file labels and file icons in the IDE and providing
80 * IDE with menu items.
82 * @author Maros Sandor
85 public class GitAnnotator
extends VCSAnnotator
{
87 private static final int INITIAL_ACTION_ARRAY_LENGTH
= 25;
88 private static final int STATUS_BADGEABLE
=
89 StatusInfo
.STATUS_VERSIONED_UPTODATE
|
90 StatusInfo
.STATUS_NOTVERSIONED_NEWLOCALLY
|
91 StatusInfo
.STATUS_VERSIONED_MODIFIEDLOCALLY
;
92 public static String ANNOTATION_REVISION
= "revision"; // NOI18N
93 public static String ANNOTATION_STATUS
= "status"; // NOI18N
94 public static String ANNOTATION_FOLDER
= "folder"; // NOI18N
95 public static String
[] LABELS
= new String
[]{ANNOTATION_REVISION
, ANNOTATION_STATUS
, ANNOTATION_FOLDER
};
96 private StatusCache cache
;
97 private File folderToScan
;
98 private ConcurrentLinkedQueue
<File
> dirsToScan
= new ConcurrentLinkedQueue
<File
>();
99 private RequestProcessor
.Task scanTask
;
100 private static final RequestProcessor rp
= new RequestProcessor("GitAnnotateScan", 1, true); // NOI18N
101 private final HtmlFormatter format
;
103 public GitAnnotator() {
104 format
= HtmlFormatter
.getInstance();
105 cache
= Git
.getInstance().getStatusCache();
106 scanTask
= rp
.create(new ScanTask());
110 public String
annotateName(String name
, VCSContext context
) {
111 int includeStatus
= StatusInfo
.STATUS_VERSIONED_UPTODATE
| StatusInfo
.STATUS_LOCAL_CHANGE
| StatusInfo
.STATUS_NOTVERSIONED_EXCLUDED
;
113 StatusInfo mostImportantInfo
= null;
114 File mostImportantFile
= null;
115 boolean folderAnnotation
= false;
117 for (final File file
: context
.getRootFiles()) {
118 StatusInfo info
= cache
.getCachedStatus(file
, true);
120 File parentFile
= file
.getParentFile();
121 Git
.LOG
.log(Level
.FINE
, "null cached status for: {0} {1} {2}", new Object
[]{file
, folderToScan
, parentFile
});
122 folderToScan
= parentFile
;
123 reScheduleScan(1000);
124 info
= new StatusInfo(StatusInfo
.STATUS_VERSIONED_UPTODATE
, false);
126 int status
= info
.getStatus();
127 if ((status
& includeStatus
) == 0) {
130 if (isMoreImportant(info
, mostImportantInfo
)) {
131 mostImportantInfo
= info
;
132 mostImportantFile
= file
;
133 folderAnnotation
= file
.isDirectory();
137 if (folderAnnotation
== false && context
.getRootFiles().size() > 1) {
138 folderAnnotation
= !Utils
.shareCommonDataObject(context
.getRootFiles().toArray(new File
[context
.getRootFiles().size()]));
140 if (mostImportantInfo
== null) {
143 if (folderAnnotation
) {
144 return format
.annotateFolderNameHtml(name
, mostImportantInfo
, mostImportantFile
);
146 return format
.annotateNameHtml(name
, mostImportantInfo
, mostImportantFile
);
150 public Image
annotateIcon(Image icon
, VCSContext context
) {
151 boolean folderAnnotation
= false;
152 for (File file
: context
.getRootFiles()) {
153 if (file
.isDirectory()) {
154 folderAnnotation
= true;
159 if (folderAnnotation
== false && context
.getRootFiles().size() > 1) {
160 folderAnnotation
= !Utils
.shareCommonDataObject(context
.getRootFiles().toArray(new File
[context
.getRootFiles().size()]));
162 if (folderAnnotation
== false) {
165 boolean isVersioned
= false;
166 for (File file
: context
.getRootFiles()) {
167 // There is an assumption here that annotateName was already
168 // called and StatusCache.getStatus was scheduled if
169 // StatusCache.getCachedStatus returned null.
170 StatusInfo info
= cache
.getCachedStatus(file
, true);
171 if ((info
!= null && (info
.getStatus() & STATUS_BADGEABLE
) != 0)) {
179 boolean allExcluded
= true;
180 boolean modified
= false;
182 Map
<File
, StatusInfo
> map
= cache
.getAllModifiedFiles();
183 Map
<File
, StatusInfo
> modifiedFiles
= new HashMap
<File
, StatusInfo
>();
184 for (File file
: map
.keySet()) {
185 StatusInfo info
= map
.get(file
);
186 if ((info
.getStatus() & StatusInfo
.STATUS_LOCAL_CHANGE
) != 0) {
187 modifiedFiles
.put(file
, info
);
191 for (File file
: context
.getRootFiles()) {
192 if (VersioningSupport
.isFlat(file
)) {
193 for (File mf
: modifiedFiles
.keySet()) {
194 if (mf
.getParentFile().equals(file
)) {
195 StatusInfo info
= modifiedFiles
.get(mf
);
196 if (info
.isDirectory()) {
199 int status
= info
.getStatus();
200 if (status
== StatusInfo
.STATUS_VERSIONED_CONFLICT
) {
201 Image badge
= ImageUtilities
.loadImage("org/nbgit/resources/icons/conflicts-badge.png", true); // NOI18N
202 return ImageUtilities
.mergeImages(icon
, badge
, 16, 9);
205 allExcluded
&= isExcludedFromCommit(mf
.getAbsolutePath());
209 for (File mf
: modifiedFiles
.keySet()) {
210 if (Utils
.isAncestorOrEqual(file
, mf
)) {
211 StatusInfo info
= modifiedFiles
.get(mf
);
212 int status
= info
.getStatus();
213 if ((status
== StatusInfo
.STATUS_NOTVERSIONED_NEWLOCALLY
|| status
== StatusInfo
.STATUS_VERSIONED_ADDEDLOCALLY
) && file
.equals(mf
)) {
216 if (status
== StatusInfo
.STATUS_VERSIONED_CONFLICT
) {
217 Image badge
= ImageUtilities
.loadImage("org/nbgit/resources/icons/conflicts-badge.png", true); // NOI18N
218 return ImageUtilities
.mergeImages(icon
, badge
, 16, 9);
221 allExcluded
&= isExcludedFromCommit(mf
.getAbsolutePath());
227 if (modified
&& !allExcluded
) {
228 Image badge
= ImageUtilities
.loadImage("org/nbgit/resources/icons/modified-badge.png", true); // NOI18N
229 return ImageUtilities
.mergeImages(icon
, badge
, 16, 9);
236 public Action
[] getActions(VCSContext ctx
, VCSAnnotator
.ActionDestination destination
) {
237 // TODO: get resource strings for all actions:
238 ResourceBundle loc
= NbBundle
.getBundle(GitAnnotator
.class);
239 Node
[] nodes
= ctx
.getElements().lookupAll(Node
.class).toArray(new Node
[0]);
240 File
[] files
= ctx
.getRootFiles().toArray(new File
[ctx
.getRootFiles().size()]);
241 File root
= GitUtils
.getRootFile(ctx
);
242 boolean noneVersioned
= root
== null;
243 boolean onlyFolders
= onlyFolders(files
);
244 boolean onlyProjects
= onlyProjects(nodes
);
246 List
<Action
> actions
= new ArrayList
<Action
>(INITIAL_ACTION_ARRAY_LENGTH
);
247 if (destination
== VCSAnnotator
.ActionDestination
.MainMenu
) {
248 actions
.add(new InitAction(loc
.getString("CTL_MenuItem_Create"), ctx
)); // NOI18N
250 actions
.add(new StatusAction(loc
.getString("CTL_PopupMenuItem_Status"), ctx
)); // NOI18N
251 actions
.add(new DiffAction(loc
.getString("CTL_PopupMenuItem_Diff"), ctx
)); // NOI18N
252 actions
.add(new UpdateAction(loc
.getString("CTL_PopupMenuItem_Update"), ctx
)); // NOI18N
253 actions
.add(new CommitAction(loc
.getString("CTL_PopupMenuItem_Commit"), ctx
)); // NOI18N
256 actions.add(new ExportDiffAction(loc.getString("CTL_PopupMenuItem_ExportDiff"), ctx)); // NOI18N
257 actions.add(new ApplyDiffAction(loc.getString("CTL_PopupMenuItem_ImportDiff"), ctx)); // NOI18N
261 actions
.add(new CloneAction(NbBundle
.getMessage(GitAnnotator
.class, "CTL_PopupMenuItem_CloneLocal", // NOI18N
262 root
.getName()), ctx
));
264 actions
.add(new CloneExternalAction(loc
.getString("CTL_PopupMenuItem_CloneOther"), ctx
)); // NOI18N
267 actions.add(new FetchAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_FetchLocal"), ctx)); // NOI18N
268 actions.add(new PushAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_PushLocal"), ctx)); // NOI18N
269 actions.add(new PushOtherAction(loc.getString("CTL_PopupMenuItem_PushOther"), ctx)); // NOI18N
270 actions.add(new PullAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_PullLocal"), ctx)); // NOI18N
271 actions.add(new PullOtherAction(loc.getString("CTL_PopupMenuItem_PullOther"), ctx)); // NOI18N
272 actions.add(new MergeAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Merge"), ctx)); // NOI18N
274 AnnotateAction tempA = new AnnotateAction(loc.getString("CTL_PopupMenuItem_ShowAnnotations"), ctx); // NOI18N
275 if (tempA.visible(nodes))
276 tempA = new AnnotateAction(loc.getString("CTL_PopupMenuItem_HideAnnotations"), ctx);
279 actions
.add(new BrowserAction(loc
.getString("CTL_PopupMenuItem_Browser"), ctx
)); // NOI18N
280 actions
.add(new LogAction(loc
.getString("CTL_PopupMenuItem_Log"), ctx
)); // NOI18N
282 actions.add(new IncomingAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_ShowIncoming"), ctx)); // NOI18N
283 actions.add(new OutAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_ShowOut"), ctx)); // NOI18N
284 actions.add(new ViewAction(loc.getString("CTL_PopupMenuItem_View"), ctx)); // NOI18N
287 actions
.add(new RevertModificationsAction(NbBundle
.getMessage(GitAnnotator
.class, "CTL_PopupMenuItem_Revert"), ctx
)); // NOI18N
289 actions.add(new StashAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Stash"), ctx));
290 actions.add(new StripAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Strip"), ctx)); // NOI18N
291 actions.add(new BackoutAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Backout"), ctx)); // NOI18N
292 actions.add(new RollbackAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Rollback"), ctx)); // NOI18N
293 actions.add(new ResolveConflictsAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Resolve"), ctx)); // NOI18N
296 if (!onlyProjects && !onlyFolders) {
297 IgnoreAction tempIA = new IgnoreAction(loc.getString("CTL_PopupMenuItem_Ignore"), ctx); // NOI18N
302 actions
.add(new CustomMenu(ctx
, true));
304 actions
.add(new PropertiesAction(loc
.getString("CTL_PopupMenuItem_Properties"), ctx
)); // NOI18N
305 } else if (noneVersioned
) {
306 actions
.add(new InitAction(loc
.getString("CTL_PopupMenuItem_Create"), ctx
));
308 actions
.add(new StatusAction(loc
.getString("CTL_PopupMenuItem_Status"), ctx
)); // NOI18N
309 actions
.add(new DiffAction(loc
.getString("CTL_PopupMenuItem_Diff"), ctx
)); // NOI18N
310 actions
.add(new UpdateAction(loc
.getString("CTL_PopupMenuItem_Update"), ctx
)); // NOI18N
311 actions
.add(new CommitAction(loc
.getString("CTL_PopupMenuItem_Commit"), ctx
)); // NOI18N
315 actions.add(new CloneAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_CloneLocal", // NOI18N
316 root.getName()), ctx));
319 actions.add(new FetchAction(NbBundle.getMessage(GitAnnotator.class,
320 "CTL_PopupMenuItem_FetchLocal"), ctx)); // NOI18N
321 actions.add(new PushAction(NbBundle.getMessage(GitAnnotator.class,
322 "CTL_PopupMenuItem_PushLocal"), ctx)); // NOI18N
323 actions.add(new PullAction(NbBundle.getMessage(GitAnnotator.class,
324 "CTL_PopupMenuItem_PullLocal"), ctx)); // NOI18N
325 actions.add(new MergeAction(NbBundle.getMessage(GitAnnotator.class,
326 "CTL_PopupMenuItem_Merge"), ctx)); // NOI18N
329 AnnotateAction tempA = new AnnotateAction(loc.getString("CTL_PopupMenuItem_ShowAnnotations"), ctx); // NOI18N
330 if (tempA.visible(nodes))
331 tempA = new AnnotateAction(loc.getString("CTL_PopupMenuItem_HideAnnotations"), ctx);
335 actions
.add(new BrowserAction(loc
.getString("CTL_PopupMenuItem_Browser"), ctx
)); // NOI18N
336 actions
.add(new LogAction(loc
.getString("CTL_PopupMenuItem_Log"), ctx
)); // NOI18N
338 actions.add(new IncomingAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_ShowIncoming"), ctx)); // NOI18N
339 actions.add(new OutAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_ShowOut"), ctx)); // NOI18N
340 actions.add(new ViewAction(loc.getString("CTL_PopupMenuItem_View"), ctx)); // NOI18N
343 actions
.add(new RevertModificationsAction(NbBundle
.getMessage(GitAnnotator
.class,
344 "CTL_PopupMenuItem_Revert"), ctx
)); // NOI18N
346 actions.add(new StripAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Strip"), ctx)); // NOI18N
347 actions.add(new BackoutAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Backout"), ctx)); // NOI18N
348 actions.add(new RollbackAction(NbBundle.getMessage(GitAnnotator.class, "CTL_PopupMenuItem_Rollback"), ctx)); // NOI18N
349 actions.add(new ResolveConflictsAction(NbBundle.getMessage(GitAnnotator.class,
350 "CTL_PopupMenuItem_Resolve"), ctx)); // NOI18N
351 if (!onlyProjects && !onlyFolders) {
352 actions.add(new ConflictResolvedAction(NbBundle.getMessage(GitAnnotator.class,
353 "CTL_PopupMenuItem_MarkResolved"), ctx));
357 if (!onlyProjects && !onlyFolders) {
358 IgnoreAction tempIA = new IgnoreAction(loc.getString("CTL_PopupMenuItem_Ignore"), ctx); // NOI18N
363 actions
.add(new PropertiesAction(loc
.getString("CTL_PopupMenuItem_Properties"), ctx
)); // NOI18N
366 return actions
.toArray(new Action
[actions
.size()]);
369 private boolean isMoreImportant(StatusInfo a
, StatusInfo b
) {
376 return GitUtils
.getComparableStatus(a
.getStatus()) < GitUtils
.getComparableStatus(b
.getStatus());
379 private boolean isExcludedFromCommit(String absolutePath
) {
383 private boolean isNothingVersioned(File
[] files
) {
384 for (File file
: files
) {
385 if ((cache
.getStatus(file
).getStatus() & StatusInfo
.STATUS_MANAGED
) != 0) {
392 private static boolean onlyProjects(Node
[] nodes
) {
396 for (Node node
: nodes
) {
397 if (node
.getLookup().lookup(Project
.class) == null) {
404 private boolean onlyFolders(File
[] files
) {
405 for (int i
= 0; i
< files
.length
; i
++) {
406 if (files
[i
].isFile()) {
409 if (!files
[i
].exists() && !cache
.getStatus(files
[i
]).isDirectory()) {
416 private void reScheduleScan(int delayMillis
) {
417 File dirToScan
= dirsToScan
.peek();
418 if (!folderToScan
.equals(dirToScan
)) {
419 if (!dirsToScan
.offer(folderToScan
)) {
420 Git
.LOG
.log(Level
.FINE
, "reScheduleScan failed to add to dirsToScan queue: {0} ", folderToScan
);
423 scanTask
.schedule(delayMillis
);
426 private class ScanTask
implements Runnable
{
429 Thread
.interrupted();
430 File dirToScan
= dirsToScan
.poll();
431 if (dirToScan
!= null) {
432 cache
.getScannedFiles(dirToScan
, null);
433 dirToScan
= dirsToScan
.peek();
434 if (dirToScan
!= null) {
435 scanTask
.schedule(1000);