Update README with note on official Oracle plugin
[nbgit.git] / src / org / nbgit / util / GitUtils.java
blob17a49a808cf7c786caa0280df6475879cad7a611
1 /*
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]"
24 * Contributor(s):
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;
44 import java.io.File;
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;
53 import java.util.Map;
54 import java.util.Set;
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;
64 import org.nbgit.Git;
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;
88 /**
90 * @author jrice
91 * @author alexbcoles
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
99 /**
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) {
115 if (list == null) {
116 return null;
118 List<String> out = new ArrayList<String>(list.size());
119 for (String s : list) {
120 out.add(replaceHttpPassword(s));
122 return out;
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) {
142 return false;
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()) {
149 return true;
152 return false;
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) {
167 return true;
168 } else {
169 return false;
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();
191 panel.add(comp);
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)));
197 return panel;
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("\\\\", "\\");
210 return path;
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) {
220 if (file == null) {
221 return false;
223 Git git = Git.getInstance();
225 if ((git.getStatusCache().getStatus(file).getStatus() & StatusInfo.STATUS_VERSIONED_ADDEDLOCALLY) != 0) {
226 return true;
227 } else {
228 return false;
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>());
247 } else {
248 File par = file.getParentFile();
249 if (par != null) {
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;
286 return context;
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
300 itor tabs #66700).
302 public static VCSContext getCurrentContext(Node[] nodes) {
303 if (nodes == null) {
304 nodes = TopComponent.getRegistry().getActivatedNodes();
306 return VCSContext.forNodes(nodes);
310 * Returns path to repository root or null if not managed
312 * @param VCSContext
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
323 * @param VCSContext
324 * @return String of repository root path
326 public static File getRootFile(VCSContext context) {
327 if (context == null) {
328 return null;
330 Git git = Git.getInstance();
331 File[] files = context.getRootFiles().toArray(new File[context.getRootFiles().size()]);
332 if (files == null || files.length == 0) {
333 return null;
335 return git.getTopmostManagedParent(files[0]);
339 * Returns File object for Project Directory
341 * @param VCSContext
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) {
350 return 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));
360 if (p != null) {
361 return p;
362 } else {
363 Git.LOG.log(Level.FINE, "GitUtils.getProjectFile(): No project for {0}", // NOI18N
364 file);
367 return null;
370 public static File getProjectFile(Project project) {
371 if (project == null) {
372 return null;
374 FileObject fo = project.getProjectDirectory();
375 return FileUtil.toFile(fo);
378 public static File[] getProjectRootFiles(Project project) {
379 if (project == null) {
380 return 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);
390 set.add(rootFile);
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
409 * @return void
411 public static void forceStatusRefresh(File file) {
412 if (Git.getInstance().isAdministrative(file)) {
413 return;
415 StatusCache cache = Git.getInstance().getStatusCache();
417 cache.refreshCached(file);
418 File repository = Git.getInstance().getTopmostManagedParent(file);
419 if (repository == null) {
420 return;
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.
435 * @return void
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.
447 * @return void
449 public static void forceStatusRefreshProject(VCSContext context) {
450 Project project = getProject(context);
451 if (project == null) {
452 return;
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)) {
470 return true;
473 return false;
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) {
484 if (file == null) {
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);
499 } else {
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:
510 * <ul>
511 * <li>revert package folder properties AND
512 * <li>revert all modified (including deleted) files in the folder
513 * </ul>
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++) {
522 File dir = files[i];
523 StatusInfo info = cache.getStatus(dir);
524 if ((status & info.getStatus()) != 0) {
525 ret.add(dir);
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) {
532 ret.add(entry);
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++) {
552 File file = all[i];
553 String path = file.getAbsolutePath();
554 if (GitModuleConfig.getDefault().isExcludedFromCommit(path) == false) {
555 files.add(file);
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)) {
563 files.add(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);
577 if (fo == null) {
578 return false;
580 try {
581 DataObject dao = DataObject.find(fo);
582 return dao.getCookie(EditorCookie.class) == null;
583 } catch (DataObjectNotFoundException e) {
584 // not found, continue
586 return false;
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++) {
596 int ch = buffer[i];
597 if (ch < 32 && ch != '\t' && ch != '\n' && ch != '\r') {
598 return true;
601 return false;
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")) {
611 return null;
613 if (GitRepository.REVISION_CURRENT.equals(revision)) {
614 return base;
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).
623 revision(revision).
624 file(base, tempFile).
625 checkout();
626 if (tempFile.length() == 0) {
627 return null;
629 return tempFile;
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)) {
650 return 0;
651 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_MERGE)) {
652 return 1;
653 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_DELETEDLOCALLY)) {
654 return 10;
655 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_REMOVEDLOCALLY)) {
656 return 11;
657 } else if (0 != (status & StatusInfo.STATUS_NOTVERSIONED_NEWLOCALLY)) {
658 return 12;
659 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_COPIEDLOCALLY)) {
660 return 13;
661 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_ADDEDLOCALLY)) {
662 return 14;
663 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_MODIFIEDLOCALLY)) {
664 return 15;
665 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_REMOVEDINREPOSITORY)) {
666 return 30;
667 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_NEWINREPOSITORY)) {
668 return 31;
669 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_MODIFIEDINREPOSITORY)) {
670 return 32;
671 } else if (0 != (status & StatusInfo.STATUS_VERSIONED_UPTODATE)) {
672 return 50;
673 } else if (0 != (status & StatusInfo.STATUS_NOTVERSIONED_EXCLUDED)) {
674 return 100;
675 } else if (0 != (status & StatusInfo.STATUS_NOTVERSIONED_NOTMANAGED)) {
676 return 101;
677 } else if (status == StatusInfo.STATUS_UNKNOWN) {
678 return 102;
679 } else {
680 throw new IllegalArgumentException("Uncomparable status: " + status);
684 protected static int getFileEnabledStatus() {
685 return ~0;
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('@');
700 if (idx < 0) {
701 return host;
702 } else {
703 return host.substring(idx + 1);
708 * This utility class should not be instantiated anywhere.
710 private GitUtils() {