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.
42 package org
.nbgit
.util
;
45 import java
.io
.IOException
;
46 import java
.util
.ArrayList
;
47 import java
.util
.Collection
;
48 import java
.util
.Comparator
;
49 import java
.util
.HashMap
;
50 import java
.util
.HashSet
;
51 import java
.util
.LinkedList
;
52 import java
.util
.List
;
55 import java
.util
.logging
.Level
;
56 import java
.util
.regex
.Matcher
;
57 import java
.util
.regex
.Pattern
;
58 import javax
.swing
.BorderFactory
;
59 import javax
.swing
.JComponent
;
60 import javax
.swing
.JOptionPane
;
61 import javax
.swing
.JPanel
;
62 import javax
.swing
.SwingConstants
;
63 import org
.jdesktop
.layout
.LayoutStyle
;
65 import org
.nbgit
.GitRepository
;
66 import org
.nbgit
.StatusCache
;
67 import org
.nbgit
.GitModuleConfig
;
68 import org
.nbgit
.StatusInfo
;
69 import org
.nbgit
.client
.CheckoutBuilder
;
70 import org
.nbgit
.ui
.status
.SyncFileNode
;
71 import org
.netbeans
.api
.project
.FileOwnerQuery
;
72 import org
.netbeans
.api
.project
.Project
;
73 import org
.netbeans
.api
.project
.ProjectUtils
;
74 import org
.netbeans
.api
.project
.SourceGroup
;
75 import org
.netbeans
.api
.project
.Sources
;
76 import org
.netbeans
.modules
.versioning
.spi
.VCSContext
;
77 import org
.openide
.cookies
.EditorCookie
;
78 import org
.openide
.filesystems
.FileObject
;
79 import org
.openide
.filesystems
.FileUtil
;
80 import org
.openide
.loaders
.DataObject
;
81 import org
.openide
.loaders
.DataObjectNotFoundException
;
82 import org
.openide
.nodes
.Node
;
83 import org
.openide
.util
.NbBundle
;
84 import org
.openide
.util
.Utilities
;
85 import org
.openide
.windows
.TopComponent
;
86 import org
.eclipse
.jgit
.lib
.Constants
;
93 public class GitUtils
{
95 private static final Pattern httpPasswordPattern
= Pattern
.compile("(https*://)(\\w+\\b):(\\b\\S*)@"); //NOI18N
96 private static final String httpPasswordReplacementStr
= "$1$2:\\*\\*\\*\\*@"; //NOI18N
97 private static final Pattern metadataPattern
= Pattern
.compile(".*\\" + File
.separatorChar
+ "(\\.)git(\\" + File
.separatorChar
+ ".*|$)"); // NOI18N
100 * replaceHttpPassword - replace any http or https passwords in the string
102 * @return String modified string with **** instead of passwords
104 public static String
replaceHttpPassword(String s
) {
105 Matcher m
= httpPasswordPattern
.matcher(s
);
106 return m
.replaceAll(httpPasswordReplacementStr
);
110 * replaceHttpPassword - replace any http or https passwords in the List<String>
112 * @return List<String> containing modified strings with **** instead of passwords
114 public static List
<String
> replaceHttpPassword(List
<String
> list
) {
118 List
<String
> out
= new ArrayList
<String
>(list
.size());
119 for (String s
: list
) {
120 out
.add(replaceHttpPassword(s
));
126 * isInUserPath - check if passed in name is on the Users PATH environment setting
128 * @param name to check
129 * @return boolean true - on PATH, false - not on PATH
131 public static boolean isInUserPath(String name
) {
132 String pathEnv
= System
.getenv().get("PATH");// NOI18N
133 // Work around issues on Windows fetching PATH
134 if (pathEnv
== null) {
135 pathEnv
= System
.getenv().get("Path");// NOI18N
137 if (pathEnv
== null) {
138 pathEnv
= System
.getenv().get("path");// NOI18N
140 String pathSeparator
= System
.getProperty("path.separator");// NOI18N
141 if (pathEnv
== null || pathSeparator
== null) {
144 String
[] paths
= pathEnv
.split(pathSeparator
);
145 for (String path
: paths
) {
146 File f
= new File(path
, name
);
147 // On Windows isFile will fail on gitk.cmd use !isDirectory
148 if (f
.exists() && !f
.isDirectory()) {
156 * confirmDialog - display a confirmation dialog
158 * @param bundleLocation location of string resources to display
159 * @param title of dialog to display
160 * @param query ask user
161 * @return boolean true - answered Yes, false - answered No
163 public static boolean confirmDialog(Class bundleLocation
, String title
, String query
) {
164 int response
= JOptionPane
.showOptionDialog(null, NbBundle
.getMessage(bundleLocation
, query
), NbBundle
.getMessage(bundleLocation
, title
), JOptionPane
.YES_NO_OPTION
, JOptionPane
.QUESTION_MESSAGE
, null, null, null);
166 if (response
== JOptionPane
.YES_OPTION
) {
174 * warningDialog - display a warning dialog
176 * @param bundleLocation location of string resources to display
177 * @param title of dialog to display
178 * @param warning to display to the user
180 public static void warningDialog(Class bundleLocation
, String title
, String warning
) {
181 JOptionPane
.showMessageDialog(null,
182 NbBundle
.getMessage(bundleLocation
, warning
),
183 NbBundle
.getMessage(bundleLocation
, title
),
184 JOptionPane
.WARNING_MESSAGE
);
187 public static JComponent
addContainerBorder(JComponent comp
) {
188 final LayoutStyle layoutStyle
= LayoutStyle
.getSharedInstance();
190 JPanel panel
= new JPanel();
192 panel
.setBorder(BorderFactory
.createEmptyBorder(
193 layoutStyle
.getContainerGap(comp
, SwingConstants
.NORTH
, null),
194 layoutStyle
.getContainerGap(comp
, SwingConstants
.WEST
, null),
195 layoutStyle
.getContainerGap(comp
, SwingConstants
.SOUTH
, null),
196 layoutStyle
.getContainerGap(comp
, SwingConstants
.EAST
, null)));
201 * stripDoubleSlash - converts '\\' to '\' in path on Windows
203 * @param String path to convert
204 * @return String converted path
206 public static String
stripDoubleSlash(String path
) {
207 if (Utilities
.isWindows()) {
208 return path
.replace("\\\\", "\\");
214 * isLocallyAdded - checks to see if this file has been Locally Added to Git
216 * @param file to check
217 * @return boolean true - ignore, false - not ignored
219 public static boolean isLocallyAdded(File file
) {
223 Git git
= Git
.getInstance();
225 if ((git
.getStatusCache().getStatus(file
).getStatus() & StatusInfo
.STATUS_VERSIONED_ADDEDLOCALLY
) != 0) {
233 * Returns a Map keyed by Directory, containing a single File/StatusInfo Map for each Directories file contents.
235 * @param Map of <File, StatusInfo> interestingFiles to be processed and divided up into Files in Directory
236 * @param Collection of <File> files to be processed against the interestingFiles
237 * @return Map of Dirs containing Map of files and status for all files in each directory
239 public static Map
<File
, Map
<File
, StatusInfo
>> getInterestingDirs(Map
<File
, StatusInfo
> interestingFiles
, Collection
<File
> files
) {
240 Map
<File
, Map
<File
, StatusInfo
>> interestingDirs
= new HashMap
<File
, Map
<File
, StatusInfo
>>();
242 for (File file
: files
) {
243 if (file
.isDirectory()) {
244 if (interestingDirs
.get(file
) == null) {
245 interestingDirs
.put(file
, new HashMap
<File
, StatusInfo
>());
248 File par
= file
.getParentFile();
250 if (interestingDirs
.get(par
) == null) {
251 interestingDirs
.put(par
, new HashMap
<File
, StatusInfo
>());
253 StatusInfo fi
= interestingFiles
.get(file
);
254 interestingDirs
.get(par
).put(file
, fi
);
259 return interestingDirs
;
263 * Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
264 * method returns File objects instead of Nodes. Every node is examined for Files it represents. File and Folder
265 * nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
266 * logical nodes must provide FileObjects in their Lookup.
268 * @param nodes null (then taken from windowsystem, it may be wrong on editor tabs #66700).
269 * @param includingFileStatus if any activated file does not have this CVS status, an empty array is returned
270 * @param includingFolderStatus if any activated folder does not have this CVS status, an empty array is returned
271 * @return File [] array of activated files, or an empty array if any of examined files/folders does not have given status
273 public static VCSContext
getCurrentContext(Node
[] nodes
, int includingFileStatus
, int includingFolderStatus
) {
274 VCSContext context
= getCurrentContext(nodes
);
275 StatusCache cache
= Git
.getInstance().getStatusCache();
276 for (File file
: context
.getRootFiles()) {
277 StatusInfo fi
= cache
.getStatus(file
);
278 if (file
.isDirectory()) {
279 if ((fi
.getStatus() & includingFolderStatus
) == 0) {
280 return VCSContext
.EMPTY
;
282 } else if ((fi
.getStatus() & includingFileStatus
) == 0) {
283 return VCSContext
.EMPTY
;
290 * Semantics is similar to {@link org.openide.windows.TopComponent#getActiva
291 tedNodes()} except that this
292 * method returns File objects instead of Nodes. Every node is examined for
293 Files it represents. File and Folder
294 * nodes represent their underlying files or folders. Project nodes are repr
295 esented by their source groups. Other
296 * logical nodes must provide FileObjects in their Lookup.
298 * @return File [] array of activated files
299 * @param nodes or null (then taken from windowsystem, it may be wrong on ed
302 public static VCSContext
getCurrentContext(Node
[] nodes
) {
304 nodes
= TopComponent
.getRegistry().getActivatedNodes();
306 return VCSContext
.forNodes(nodes
);
310 * Returns path to repository root or null if not managed
313 * @return String of repository root path
315 public static String
getRootPath(VCSContext context
) {
316 File root
= getRootFile(context
);
317 return (root
== null) ?
null : root
.getAbsolutePath();
321 * Returns path to repository root or null if not managed
324 * @return String of repository root path
326 public static File
getRootFile(VCSContext context
) {
327 if (context
== null) {
330 Git git
= Git
.getInstance();
331 File
[] files
= context
.getRootFiles().toArray(new File
[context
.getRootFiles().size()]);
332 if (files
== null || files
.length
== 0) {
335 return git
.getTopmostManagedParent(files
[0]);
339 * Returns File object for Project Directory
342 * @return File object of Project Directory
344 public static File
getProjectFile(VCSContext context
) {
345 return getProjectFile(getProject(context
));
348 public static Project
getProject(VCSContext context
) {
349 if (context
== null) {
352 File
[] files
= context
.getRootFiles().toArray(new File
[context
.getRootFiles().size()]);
354 for (File file
: files
) {
355 /* We may be committing a LocallyDeleted file */
356 if (!file
.exists()) {
357 file
= file
.getParentFile();
359 Project p
= FileOwnerQuery
.getOwner(FileUtil
.toFileObject(file
));
363 Git
.LOG
.log(Level
.FINE
, "GitUtils.getProjectFile(): No project for {0}", // NOI18N
370 public static File
getProjectFile(Project project
) {
371 if (project
== null) {
374 FileObject fo
= project
.getProjectDirectory();
375 return FileUtil
.toFile(fo
);
378 public static File
[] getProjectRootFiles(Project project
) {
379 if (project
== null) {
382 Set
<File
> set
= new HashSet
<File
>();
384 Sources sources
= ProjectUtils
.getSources(project
);
385 SourceGroup
[] sourceGroups
= sources
.getSourceGroups(Sources
.TYPE_GENERIC
);
386 for (int j
= 0; j
< sourceGroups
.length
; j
++) {
387 SourceGroup sourceGroup
= sourceGroups
[j
];
388 FileObject srcRootFo
= sourceGroup
.getRootFolder();
389 File rootFile
= FileUtil
.toFile(srcRootFo
);
392 return set
.toArray(new File
[set
.size()]);
396 * Checks file location to see if it is part of Git metdata
398 * @param file file to check
399 * @return true if the file or folder is a part of Git metadata, false otherwise
401 public static boolean isPartOfGitMetadata(File file
) {
402 return metadataPattern
.matcher(file
.getAbsolutePath()).matches();
406 * Forces refresh of Status for the given directory
408 * @param start file or dir to begin refresh from
411 public static void forceStatusRefresh(File file
) {
412 if (Git
.getInstance().isAdministrative(file
)) {
415 StatusCache cache
= Git
.getInstance().getStatusCache();
417 cache
.refreshCached(file
);
418 File repository
= Git
.getInstance().getTopmostManagedParent(file
);
419 if (repository
== null) {
422 if (file
.isDirectory()) {
423 Map
<File
, StatusInfo
> interestingFiles
;
424 interestingFiles
= GitCommand
.getInterestingStatus(repository
, file
);
425 for (Map
.Entry
<File
, StatusInfo
> entry
: interestingFiles
.entrySet()) {
426 cache
.refreshFileStatus(entry
.getKey(), entry
.getValue(), null);
432 * Forces refresh of Status for the specfied context.
434 * @param VCSContext context to be updated.
437 public static void forceStatusRefresh(VCSContext context
) {
438 for (File root
: context
.getRootFiles()) {
439 forceStatusRefresh(root
);
444 * Forces refresh of Status for the project of the specified context
446 * @param VCSContext ctx whose project is be updated.
449 public static void forceStatusRefreshProject(VCSContext context
) {
450 Project project
= getProject(context
);
451 if (project
== null) {
454 File
[] files
= getProjectRootFiles(project
);
455 for (int j
= 0; j
< files
.length
; j
++) {
456 forceStatusRefresh(files
[j
]);
461 * Tests parent/child relationship of files.
463 * @param parent file to be parent of the second parameter
464 * @param file file to be a child of the first parameter
465 * @return true if the second parameter represents the same file as the first parameter OR is its descendant (child)
467 public static boolean isParentOrEqual(File parent
, File file
) {
468 for (; file
!= null; file
= file
.getParentFile()) {
469 if (file
.equals(parent
)) {
477 * Returns path of file relative to root repository or a warning message
478 * if the file is not under the repository root.
480 * @param File to get relative path from the repository root
481 * @return String of relative path of the file from teh repository root
483 public static String
getRelativePath(File file
) {
485 return NbBundle
.getMessage(SyncFileNode
.class, "LBL_Location_NotInRepository");
487 String shortPath
= file
.getAbsolutePath();
488 if (shortPath
== null) {
489 return NbBundle
.getMessage(SyncFileNode
.class, "LBL_Location_NotInRepository");
491 Git git
= Git
.getInstance();
492 File rootManagedFolder
= git
.getTopmostManagedParent(file
);
493 if (rootManagedFolder
== null) {
494 return NbBundle
.getMessage(SyncFileNode
.class, "LBL_Location_NotInRepository");
496 String root
= rootManagedFolder
.getAbsolutePath();
497 if (shortPath
.startsWith(root
) && shortPath
.length() > root
.length()) {
498 return shortPath
.substring(root
.length() + 1);
500 return NbBundle
.getMessage(SyncFileNode
.class, "LBL_Location_NotInRepository");
505 * Normalize flat files, Git treats folder as normal file
506 * so it's necessary explicitly list direct descendants to
507 * get classical flat behaviour.
509 * <p> E.g. revert on package node means:
511 * <li>revert package folder properties AND
512 * <li>revert all modified (including deleted) files in the folder
515 * @return files with given status and direct descendants with given status.
517 public static File
[] flatten(File
[] files
, int status
) {
518 LinkedList
<File
> ret
= new LinkedList
<File
>();
520 StatusCache cache
= Git
.getInstance().getStatusCache();
521 for (int i
= 0; i
< files
.length
; i
++) {
523 StatusInfo info
= cache
.getStatus(dir
);
524 if ((status
& info
.getStatus()) != 0) {
527 File
[] entries
= cache
.listFiles(dir
); // comparing to dir.listFiles() lists already deleted too
528 for (int e
= 0; e
< entries
.length
; e
++) {
529 File entry
= entries
[e
];
530 info
= cache
.getStatus(entry
);
531 if ((status
& info
.getStatus()) != 0) {
537 return ret
.toArray(new File
[ret
.size()]);
541 * Utility method that returns all non-excluded modified files that are
542 * under given roots (folders) and have one of specified statuses.
544 * @param context context to search
545 * @param includeStatus bit mask of file statuses to include in result
546 * @return File [] array of Files having specified status
548 public static File
[] getModifiedFiles(VCSContext context
, int includeStatus
) {
549 File
[] all
= Git
.getInstance().getStatusCache().listFiles(context
, includeStatus
);
550 List
<File
> files
= new ArrayList
<File
>();
551 for (int i
= 0; i
< all
.length
; i
++) {
553 String path
= file
.getAbsolutePath();
554 if (GitModuleConfig
.getDefault().isExcludedFromCommit(path
) == false) {
559 // ensure that command roots (files that were explicitly selected by user) are included in Diff
560 StatusCache cache
= Git
.getInstance().getStatusCache();
561 for (File file
: context
.getRootFiles()) {
562 if (file
.isFile() && (cache
.getStatus(file
).getStatus() & includeStatus
) != 0 && !files
.contains(file
)) {
566 return files
.toArray(new File
[files
.size()]);
570 * Checks if the file is binary.
572 * @param file file to check
573 * @return true if the file cannot be edited in NetBeans text editor, false otherwise
575 public static boolean isFileContentBinary(File file
) {
576 FileObject fo
= FileUtil
.toFileObject(file
);
581 DataObject dao
= DataObject
.find(fo
);
582 return dao
.getCookie(EditorCookie
.class) == null;
583 } catch (DataObjectNotFoundException e
) {
584 // not found, continue
590 * @return true if the buffer is almost certainly binary.
591 * Note: Non-ASCII based encoding encoded text is binary,
592 * newlines cannot be reliably detected.
594 public static boolean isBinary(byte[] buffer
) {
595 for (int i
= 0; i
< buffer
.length
; i
++) {
597 if (ch
< 32 && ch
!= '\t' && ch
!= '\n' && ch
!= '\r') {
605 * Loads the file in specified revision.
607 * @return null if the file does not exist in given revision
609 public static File
getFileRevision(File base
, String revision
) throws IOException
{
610 if (revision
.equals("-1")) {
613 if (GitRepository
.REVISION_CURRENT
.equals(revision
)) {
616 if (GitRepository
.REVISION_BASE
.equals(revision
)) {
617 revision
= Constants
.HEAD
;
619 File tempFile
= File
.createTempFile("tmp", "-" + base
.getName()); //NOI18N
620 File repository
= Git
.getInstance().getTopmostManagedParent(base
);
622 CheckoutBuilder
.create(repository
).
624 file(base
, tempFile
).
626 if (tempFile
.length() == 0) {
633 * Compares two {@link StatusInfo} objects by importance of statuses they represent.
635 public static class ByImportanceComparator
<T
> implements Comparator
<StatusInfo
> {
637 public int compare(StatusInfo i1
, StatusInfo i2
) {
638 return getComparableStatus(i1
.getStatus()) - getComparableStatus(i2
.getStatus());
643 * Gets integer status that can be used in comparators. The more important the status is for the user,
644 * the lower value it has. Conflict is 0, unknown status is 100.
646 * @return status constant suitable for 'by importance' comparators
648 public static int getComparableStatus(int status
) {
649 if (0 != (status
& StatusInfo
.STATUS_VERSIONED_CONFLICT
)) {
651 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_MERGE
)) {
653 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_DELETEDLOCALLY
)) {
655 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_REMOVEDLOCALLY
)) {
657 } else if (0 != (status
& StatusInfo
.STATUS_NOTVERSIONED_NEWLOCALLY
)) {
659 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_COPIEDLOCALLY
)) {
661 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_ADDEDLOCALLY
)) {
663 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_MODIFIEDLOCALLY
)) {
665 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_REMOVEDINREPOSITORY
)) {
667 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_NEWINREPOSITORY
)) {
669 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_MODIFIEDINREPOSITORY
)) {
671 } else if (0 != (status
& StatusInfo
.STATUS_VERSIONED_UPTODATE
)) {
673 } else if (0 != (status
& StatusInfo
.STATUS_NOTVERSIONED_EXCLUDED
)) {
675 } else if (0 != (status
& StatusInfo
.STATUS_NOTVERSIONED_NOTMANAGED
)) {
677 } else if (status
== StatusInfo
.STATUS_UNKNOWN
) {
680 throw new IllegalArgumentException("Uncomparable status: " + status
);
684 protected static int getFileEnabledStatus() {
688 protected static int getDirectoryEnabledStatus() {
689 return StatusInfo
.STATUS_MANAGED
& ~StatusInfo
.STATUS_NOTVERSIONED_EXCLUDED
;
693 * Rips an eventual username off - e.g. user@svn.host.org
695 * @param host - hostname with a userneame
696 * @return host - hostname without the username
698 public static String
ripUserFromHost(String host
) {
699 int idx
= host
.indexOf('@');
703 return host
.substring(idx
+ 1);
708 * This utility class should not be instantiated anywhere.