org.netbeans.modules.git.utils.GitUtils Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.git.utils;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.netbeans.api.queries.SharabilityQuery;
import org.netbeans.libs.git.GitBranch;
import org.netbeans.libs.git.GitException;
import org.netbeans.libs.git.GitRemoteConfig;
import org.netbeans.libs.git.GitRevisionInfo;
import org.netbeans.libs.git.GitURI;
import org.netbeans.libs.git.progress.ProgressMonitor;
import org.netbeans.modules.git.FileInformation;
import org.netbeans.modules.git.FileInformation.Status;
import org.netbeans.modules.git.FileStatusCache;
import org.netbeans.modules.git.Git;
import org.netbeans.modules.git.GitModuleConfig;
import org.netbeans.modules.git.VersionsCache;
import org.netbeans.modules.git.ui.blame.AnnotateAction;
import org.netbeans.modules.git.ui.commit.CommitAction;
import org.netbeans.modules.git.ui.ignore.IgnoreAction;
import org.netbeans.modules.git.ui.repository.RepositoryInfo;
import org.netbeans.modules.git.GitStatusNode;
import org.netbeans.modules.git.client.CredentialsCallback;
import org.netbeans.modules.git.client.GitClient;
import org.netbeans.modules.git.ui.status.StatusAction;
import org.netbeans.modules.versioning.diff.DiffUtils;
import org.netbeans.modules.versioning.spi.VCSContext;
import org.netbeans.modules.versioning.util.FileSelector;
import org.netbeans.modules.versioning.util.IndexingBridge;
import org.netbeans.modules.versioning.util.Utils;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.QuickSearch;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.Line;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;
import org.openide.windows.TopComponent;
/**
*s
* @author ondra
*/
public final class GitUtils {
public static final String DOT_GIT = ".git"; //NOI18N
public static final String INDEX_LOCK = "index.lock"; //NOI18N
private static final Pattern METADATA_PATTERN = Pattern.compile(".*\\" + File.separatorChar + "(\\.)git(\\" + File.separatorChar + ".*|$)"); // NOI18N
private static final String FILENAME_GITIGNORE = ".gitignore"; // NOI18N
public static final String HEAD = "HEAD"; //NOI18N
public static final String INDEX = "INDEX"; //NOI18N
public static final String CURRENT = "CURRENT"; //NOI18N
public static final String PREFIX_R_HEADS = "refs/heads/"; //NOI18N
public static final String PREFIX_R_TAGS = "refs/tags/"; //NOI18N
public static final String PREFIX_R_REMOTES = "refs/remotes/"; //NOI18N
public static final ProgressMonitor NULL_PROGRESS_MONITOR = new NullProgressMonitor();
public static final String MASTER = "master"; //NOI18N
private static final Set loggedRepositories = new HashSet();
public static final String REMOTE_ORIGIN = "origin"; //NOI18N
public static final String ORIGIN = "origin"; //NOI18N
/**
* Checks file location to see if it is part of git metadata
*
* @param file file to check
* @return true if the file or folder is a part of git metadata, false otherwise
*/
public static boolean isPartOfGitMetadata (File file) {
return METADATA_PATTERN.matcher(file.getAbsolutePath()).matches();
}
/**
* Tests .hg directory itself.
*/
public static boolean isAdministrative (File file) {
String name = file.getName();
return isAdministrative(name) && file.isDirectory();
}
public static boolean isAdministrative (String fileName) {
return fileName.equals(DOT_GIT); // NOI18N
}
public static boolean repositoryExistsFor (File file) {
return new File(file, DOT_GIT).exists();
}
/**
* Returns the administrative git folder for the given repository and normalizes the file
* @param repositoryRoot normalized root of the repository
* @return administrative git folder
*/
public static File getGitFolderForRoot (File repositoryRoot) {
return FileUtil.normalizeFile(new File(repositoryRoot, DOT_GIT));
}
/**
* Adds the given file into filesUnderRoot:
*
* - if the file was already in the set, does nothing and returns true
* - if the file lies under a folder already present in the set, does nothing and returns true
* - if the file and none of it's ancestors is not in the set yet, this adds the file into the set,
* removes all it's children and returns false
* @param repository repository root
* @param filesUnderRoot set of repository roots
* @param file file to add
* @return false if the file was added or true if it was already contained
*/
public static boolean prepareRootFiles (File repository, Collection filesUnderRoot, File file) {
boolean added = false;
Set filesToRemove = new HashSet();
for (File fileUnderRoot : filesUnderRoot) {
if (file.equals(fileUnderRoot) || fileUnderRoot.equals(repository)) {
// file has already been inserted or scan is planned for the whole repository root
added = true;
break;
}
if (file.equals(repository)) {
// plan the scan for the whole repository root
// adding the repository, there's no need to leave all other files
filesUnderRoot.clear();
break;
} else {
if (file.getAbsolutePath().length() < fileUnderRoot.getAbsolutePath().length()) {
if (Utils.isAncestorOrEqual(file, fileUnderRoot)) {
filesToRemove.add(fileUnderRoot);
}
} else {
if (Utils.isAncestorOrEqual(fileUnderRoot, file)) {
added = true;
break;
}
}
}
}
filesUnderRoot.removeAll(filesToRemove);
if (!added) {
// not added yet
filesUnderRoot.add(file);
}
return added;
}
public static boolean isIgnored(File file, boolean checkSharability){
if (file == null) return false;
String path = file.getPath();
File topFile = Git.getInstance().getRepositoryRoot(file);
// We assume that the toplevel directory should not be ignored.
if (topFile == null || topFile.equals(file)) {
return false;
}
// check cached not sharable folders and files
if (isNotSharable(path, topFile)) {
return true;
}
// If a parent of the file matches a pattern ignore the file
File parentFile = file.getParentFile();
if (!parentFile.equals(topFile)) {
if (isIgnored(parentFile, false)) return true;
}
if (FILENAME_GITIGNORE.equals(file.getName())) return false;
if (checkSharability) {
int sharability = SharabilityQuery.getSharability(FileUtil.normalizeFile(file));
if (sharability == SharabilityQuery.NOT_SHARABLE) {
if (GitModuleConfig.getDefault().getAutoIgnoreFiles()) {
ignoreNotSharableAncestor(topFile, file);
} else {
addNotSharable(topFile, path);
}
return true;
}
}
return false;
}
/**
* Returns the remote tracked branch for the current branch.
*
* @param info
* @param errorLabel if not null a warning dialog will also be displayed.
* @return
*/
@NbBundle.Messages({
"# {0} - branch name", "MSG_Err.noTrackedBranch=No tracked remote branch specified for local {0}",
"# {0} - branch name", "MSG_Err.trackedBranchLocal=Tracked branch {0} is not a remote branch"
})
public static GitBranch getTrackedBranch (RepositoryInfo info, String errorLabel) {
GitBranch activeBranch = info.getActiveBranch();
if (activeBranch == null) {
return null;
}
GitBranch trackedBranch = activeBranch.getTrackedBranch();
if (trackedBranch == null) {
if (errorLabel != null) {
notifyError(errorLabel, Bundle.MSG_Err_noTrackedBranch(activeBranch.getName()));
}
return null;
}
if (!trackedBranch.isRemote()) {
if (errorLabel != null) {
notifyError(errorLabel, Bundle.MSG_Err_trackedBranchLocal(trackedBranch.getName()));
}
return null;
}
return trackedBranch;
}
public static void notifyError (String errorLabel, String errorMessage) {
NotifyDescriptor nd = new NotifyDescriptor(
errorMessage,
errorLabel,
NotifyDescriptor.DEFAULT_OPTION,
NotifyDescriptor.ERROR_MESSAGE,
new Object[]{NotifyDescriptor.OK_OPTION},
NotifyDescriptor.OK_OPTION);
DialogDisplayer.getDefault().notify(nd);
}
// cached not sharable files and folders
private static final Map> notSharable = Collections.synchronizedMap(new HashMap>(5));
public static String parseRemoteHeadFromFetch (String fetchRefSpec) {
if (fetchRefSpec.startsWith("+")) { //NOI18N
fetchRefSpec = fetchRefSpec.substring(1);
}
int pos = fetchRefSpec.indexOf(':');
if (pos > 0) {
return fetchRefSpec.substring(0, pos);
} else {
return null;
}
}
private static void addNotSharable (File topFile, String ignoredPath) {
synchronized (notSharable) {
// get cached patterns
Set ignores = notSharable.get(topFile);
if (ignores == null) {
ignores = new HashSet();
}
String patternCandidate = ignoredPath;
// test for duplicate patterns
for (Iterator it = ignores.iterator(); it.hasNext();) {
String storedPattern = it.next();
if (storedPattern.equals(ignoredPath) // already present
|| ignoredPath.startsWith(storedPattern + '/')) { // path already ignored by its ancestor
patternCandidate = null;
break;
} else if (storedPattern.startsWith(ignoredPath + '/')) { // stored pattern matches a subset of ignored path
// remove the stored pattern and add the ignored path
it.remove();
}
}
if (patternCandidate != null) {
ignores.add(patternCandidate);
}
notSharable.put(topFile, ignores);
}
}
private static boolean isNotSharable (String path, File topFile) {
boolean retval = false;
Set notSharablePaths = notSharable.get(topFile);
if (notSharablePaths == null) {
notSharablePaths = Collections.emptySet();
}
retval = notSharablePaths.contains(path);
return retval;
}
/**
* Permanently ignores (modifies ignore file) topmost not-sharable ancestor of a given file.
* @param topFile
* @param notSharableFile
*/
private static void ignoreNotSharableAncestor (File topFile, File notSharableFile) {
if (topFile.equals(notSharableFile)) {
throw new IllegalStateException("Trying to ignore " + notSharableFile + " in " + topFile); //NOI18N
}
File parent;
// find the topmost
while (!topFile.equals(parent = notSharableFile.getParentFile()) && SharabilityQuery.getSharability(FileUtil.normalizeFile(parent)) == SharabilityQuery.NOT_SHARABLE) {
notSharableFile = parent;
}
addNotSharable(topFile, notSharableFile.getAbsolutePath());
// ignore only folders
if (notSharableFile.isDirectory()) {
for (File f : Git.getInstance().getCreatedFolders()) {
if (Utils.isAncestorOrEqual(f, notSharableFile)) {
SystemAction.get(IgnoreAction.class).ignoreFolders(topFile, new File[] { notSharableFile });
}
}
}
}
/**
* Determines if the given context contains at least one root file from a git repository
*
* @param VCSContext
* @return true if the given context contains a root file from a git repository
*/
public static boolean isFromGitRepository (VCSContext context){
return getRootFile(context) != null;
}
/**
* Returns path to repository root or null if not managed
*
* @param VCSContext
* @return String of repository root path
*/
public static File getRootFile (VCSContext context){
if (context == null) return null;
Git git = Git.getInstance();
File [] files = context.getRootFiles().toArray(new File[context.getRootFiles().size()]);
if (files == null || files.length == 0) return null;
File root = git.getRepositoryRoot(files[0]);
return root;
}
/**
* Returns repository roots for all root files from context
*
* @param VCSContext
* @return repository roots
*/
public static Set getRepositoryRoots(VCSContext context) {
Set rootsSet = context.getRootFiles();
return getRepositoryRoots(rootsSet);
}
/**
* Returns repository roots for all root files from context
*
* @param roots root files
* @return repository roots
*/
public static Set getRepositoryRoots (Collection roots) {
Set ret = new HashSet();
// filter managed roots
for (File file : roots) {
if(Git.getInstance().isManaged(file)) {
File repoRoot = Git.getInstance().getRepositoryRoot(file);
if(repoRoot != null) {
ret.add(repoRoot);
}
}
}
return ret;
}
/**
*
* @param ctx
* @return
*/
public static HashMap.SimpleImmutableEntry getActionRoots(VCSContext ctx) {
Set rootsSet = ctx.getRootFiles();
Map> map = new HashMap>();
// filter managed roots
for (File file : rootsSet) {
if(Git.getInstance().isManaged(file)) {
File repoRoot = Git.getInstance().getRepositoryRoot(file);
if(repoRoot != null) {
List l = map.get(repoRoot);
if(l == null) {
l = new LinkedList();
map.put(repoRoot, l);
}
l.add(file);
}
}
}
Set repoRoots = map.keySet();
if(map.size() > 1) {
// more than one managed root => need a dlg
FileSelector fs = new FileSelector(
NbBundle.getMessage(GitUtils.class, "LBL_FileSelector_Title"), //NOI18N
NbBundle.getMessage(GitUtils.class, "FileSelector.jLabel1.text"), //NOI18N
new HelpCtx("org.netbeans.modules.git.FileSelector"), //NOI18N
GitModuleConfig.getDefault().getPreferences());
if(fs.show(repoRoots.toArray(new File[repoRoots.size()]))) {
File selection = fs.getSelectedFile();
List l = map.get(selection);
return new HashMap.SimpleImmutableEntry(selection, l.toArray(new File[l.size()]));
} else {
return null;
}
} else if (map.isEmpty()) {
return null;
} else {
File root = map.keySet().iterator().next();
List l = map.get(root);
return new HashMap.SimpleImmutableEntry(root, l.toArray(new File[l.size()]));
}
}
/**
* Returns only those root files from the given context which belong to repository
* @param ctx
* @param repository
* @return
*/
public static File[] filterForRepository(final VCSContext ctx, final File repository) {
File[] files = null;
if(ctx != null) {
Set s = ctx.getRootFiles();
files = s.toArray(new File[s.size()]);
}
if (files != null) {
List l = new LinkedList();
for (File file : files) {
File r = Git.getInstance().getRepositoryRoot(file);
if (r != null && r.equals(repository)) {
l.add(file);
}
}
files = l.toArray(new File[l.size()]);
}
return files;
}
/**
* Normalize flat files, Git treats folder as normal file
* so it's necessary explicitly list direct descendants to
* get classical flat behaviour.
* Does not return up-to-date files
*
* E.g. revert on package node means:
*
* - revert package folder properties AND
*
- revert all modified (including deleted) files in the folder
*
*
* @return files with given status and direct descendants with given status.
*/
public static File[] flatten(File[] files, Set statuses) {
LinkedList ret = new LinkedList();
FileStatusCache cache = Git.getInstance().getFileStatusCache();
for (int i = 0; iDoes not return up-to-date files
*/
public static File[] listFiles (File[] roots, EnumSet includedStatuses) {
File[][] split = Utils.splitFlatOthers(roots);
List fileList = new ArrayList();
FileStatusCache cache = Git.getInstance().getFileStatusCache();
for (int c = 0; c < split.length; c++) {
File[] splitRoots = split[c];
if (c == 1) {
// recursive
fileList.addAll(Arrays.asList(cache.listFiles(splitRoots, includedStatuses)));
} else {
// not recursive, list only direct descendants
fileList.addAll(Arrays.asList(GitUtils.flatten(splitRoots, includedStatuses)));
}
}
return fileList.toArray(new File[fileList.size()]);
}
/**
* Semantics is similar to {@link org.openide.windows.TopComponent#getActivatedNodes()} except that this
* method returns File objects instead of Nodes. Every node is examined for Files it represents. File and Folder
* nodes represent their underlying files or folders. Project nodes are represented by their source groups. Other
* logical nodes must provide FileObjects in their Lookup.
*
* @return File [] array of activated files
* @param nodes or null (then taken from windowsystem, it may be wrong on editor tabs #66700).
*/
public static VCSContext getCurrentContext(Node[] nodes) {
if (nodes == null) {
nodes = TopComponent.getRegistry().getActivatedNodes();
}
return VCSContext.forNodes(nodes);
}
/**
* Uses content analysis to return the mime type for files.
*
* @param file file to examine
* @return String mime type of the file (or best guess)
*/
public static String getMimeType(File file) {
FileObject fo = FileUtil.toFileObject(file);
String foMime;
boolean hasMime = false;
if (fo == null) {
foMime = "content/unknown"; // NOI18N
} else {
foMime = fo.getMIMEType();
if ("content/unknown".equals(foMime)) { // NOI18N
foMime = "text/plain"; // NOI18N
} else {
hasMime = true;
}
}
if (!hasMime) {
return isFileContentBinary(file) ? "application/octet-stream" : foMime; // NOI18N
} else {
return foMime;
}
}
/**
* Checks if the file is binary.
*
* @param file file to check
* @return true if the file cannot be edited in NetBeans text editor, false otherwise
*/
public static boolean isFileContentBinary(File file) {
FileObject fo = FileUtil.toFileObject(file);
if (fo == null) return false;
try {
DataObject dao = DataObject.find(fo);
return dao.getCookie(EditorCookie.class) == null;
} catch (DataObjectNotFoundException e) {
// not found, continue
}
return false;
}
/**
* Determines if the context has been created in a git view, i.e. it consists of instances of {@link GitStatusNode}
* @param context
* @return true if the context contains instances of {@link GitStatusNode}
*/
public static boolean isFromInternalView (VCSContext context) {
return context.getElements().lookup(GitStatusNode.class) != null;
}
public static List getRelativePaths(File workDir, File[] roots) {
List paths = new ArrayList(roots.length);
for (File root : roots) {
if (workDir.equals(root)) {
paths.clear();
break;
} else {
paths.add(getRelativePath(workDir, root));
}
}
return paths;
}
public static String getRelativePath (File repo, final File file) {
StringBuilder relativePath = new StringBuilder(""); //NOI18N
File parent = file;
if (!parent.equals(repo)) {
while (parent != null && !parent.equals(repo)) {
relativePath.insert(0, "/").insert(0, parent.getName()); //NOI18N
parent = parent.getParentFile();
}
if (parent == null) {
throw new IllegalArgumentException(file.getAbsolutePath() + " is not under " + repo.getAbsolutePath());
}
relativePath.deleteCharAt(relativePath.length() - 1);
}
return relativePath.toString();
}
public static void openInVersioningView (Collection files, File repository, ProgressMonitor pm) {
List nodes = new LinkedList();
for (File file : files) {
Node node = new AbstractNode(Children.LEAF, Lookups.fixed(file));
nodes.add(node);
// this will refresh seen roots
}
Git.getInstance().getFileStatusCache().refreshAllRoots(Collections.>singletonMap(repository, files), pm);
if (!pm.isCanceled()) {
final VCSContext context = VCSContext.forNodes(nodes.toArray(new Node[nodes.size()]));
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
SystemAction.get(StatusAction.class).performContextAction(context);
}
});
}
}
public static void printInfo (StringBuilder sb, GitRevisionInfo info) {
printInfo(sb, info, true);
}
public static void printInfo (StringBuilder sb, GitRevisionInfo info, boolean endWithNewLine) {
String lbrevision = NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.revision"); // NOI18N
String lbauthor = NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.author"); // NOI18N
String lbcommitter = NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.committer"); // NOI18N
String lbdate = NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.date"); // NOI18N
String lbsummary = NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.summary"); // NOI18N
String author = info.getAuthor().toString();
String committer = info.getCommitter().toString();
sb.append(NbBundle.getMessage(CommitAction.class, "MSG_CommitAction.logCommit.title")).append("\n"); //NOI18N
sb.append(lbrevision);
sb.append(info.getRevision());
sb.append('\n'); // NOI18N
sb.append(lbauthor);
sb.append(author);
sb.append('\n'); // NOI18N
if (!author.equals(committer)) {
sb.append(lbcommitter);
sb.append(committer);
sb.append('\n'); // NOI18N
}
sb.append(lbdate);
sb.append(DateFormat.getDateTimeInstance().format(new Date(info.getCommitTime())));
sb.append('\n'); // NOI18N
sb.append(lbsummary);
int prefixLen = lbsummary.length();
sb.append(formatMultiLine(prefixLen, info.getFullMessage()));
if (endWithNewLine) {
sb.append('\n');
}
}
private static String formatMultiLine (int prefixLen, String message) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < prefixLen; ++i) {
sb.append(" "); //NOI18N
}
String prefix = sb.toString();
String[] lines = message.split("\n"); //NOI18N
sb = new StringBuilder(lines.length > 0 ? lines[0] : ""); //NOI18N
for (int i = 1; i < lines.length; ++i) {
sb.append("\n").append(prefix).append(lines[i]); //NOI18N
}
return sb.toString();
}
/**
* Forces refresh of diff sidebars and history tabs for open files belonging to the given repositories
* @param repositories
*/
public static void headChanged (File... repositories) {
Set openFiles = Utils.getOpenFiles();
Set repositorySet = new HashSet(Arrays.asList(repositories));
for (Iterator it = openFiles.iterator(); it.hasNext(); ) {
File file = it.next();
if (!repositorySet.contains(Git.getInstance().getRepositoryRoot(file))) {
it.remove();
}
}
if (!openFiles.isEmpty()) {
Git.getInstance().headChanged(openFiles);
Git.getInstance().getHistoryProvider().fireHistoryChange(openFiles.toArray(new File[openFiles.size()]));
}
}
public static boolean isRepositoryLocked (File repository) {
return new File(getGitFolderForRoot(repository), INDEX_LOCK).exists(); //NOI18N
}
public static void openInRevision (File originalFile, String revision1, int lineNumber,
String revisionToOpen, boolean showAnnotations, ProgressMonitor pm) throws IOException {
File file1 = VersionsCache.getInstance().getFileRevision(originalFile, revision1, pm);
if (file1 == null) { // can be null if the file does not exist or is empty in the given revision
file1 = File.createTempFile("tmp", "-" + originalFile.getName(), Utils.getTempFolder()); //NOI18N
file1.deleteOnExit();
}
if (pm.isCanceled()) {
return;
}
File file = VersionsCache.getInstance().getFileRevision(originalFile, revisionToOpen, pm);
if (file == null) { // can be null if the file does not exist or is empty in the given revision
file = File.createTempFile("tmp", "-" + originalFile.getName(), Utils.getTempFolder()); //NOI18N
file.deleteOnExit();
}
if (pm.isCanceled()) {
return;
}
int matchingLine = DiffUtils.getMatchingLine(file1, file, lineNumber);
openInRevision(file, originalFile, matchingLine, revisionToOpen, showAnnotations, pm);
}
public static void openInRevision (File originalFile, int lineNumber, String revision,
boolean showAnnotations, ProgressMonitor pm) throws IOException {
File file = VersionsCache.getInstance().getFileRevision(originalFile, revision, pm);
if (pm.isCanceled()) {
return;
}
if (file == null) { // can be null if the file does not exist or is empty in the given revision
file = File.createTempFile("tmp", "-" + originalFile.getName(), Utils.getTempFolder()); //NOI18N
file.deleteOnExit();
}
openInRevision(file, originalFile, lineNumber, revision, showAnnotations, pm);
}
private static void openInRevision (final File fileToOpen, final File originalFile, final int lineNumber, final String revision, boolean showAnnotations, ProgressMonitor pm) throws IOException {
final FileObject fo = FileUtil.toFileObject(FileUtil.normalizeFile(fileToOpen));
EditorCookie ec = null;
org.openide.cookies.OpenCookie oc = null;
try {
DataObject dobj = DataObject.find(fo);
ec = dobj.getCookie(EditorCookie.class);
oc = dobj.getCookie(org.openide.cookies.OpenCookie.class);
} catch (DataObjectNotFoundException ex) {
Logger.getLogger(GitUtils.class.getName()).log(Level.FINE, null, ex);
}
if (ec == null && oc != null) {
oc.open();
} else {
CloneableEditorSupport ces = org.netbeans.modules.versioning.util.Utils.openFile(fo, revision.substring(0, 7));
if (showAnnotations && ces != null && !pm.isCanceled()) {
final org.openide.text.CloneableEditorSupport support = ces;
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
javax.swing.JEditorPane[] panes = support.getOpenedPanes();
if (panes != null) {
if (lineNumber >= 0 && lineNumber < support.getLineSet().getLines().size()) {
support.getLineSet().getCurrent(lineNumber).show(Line.ShowOpenType.NONE, Line.ShowVisibilityType.FRONT);
}
SystemAction.get(AnnotateAction.class).showAnnotations(panes[0], originalFile, revision);
}
}
});
}
}
}
public static Map> sortByRepository (Collection files) {
Map> sorted = new HashMap>(5);
for (File f : files) {
File repository = Git.getInstance().getRepositoryRoot(f);
if (repository != null) {
Set repoFiles = sorted.get(repository);
if (repoFiles == null) {
repoFiles = new HashSet();
sorted.put(repository, repoFiles);
}
repoFiles.add(f);
}
}
return sorted;
}
public static boolean contains (Collection roots, File file) {
for (File root : roots) {
if (Utils.isAncestorOrEqual(root, file)) {
return true;
}
}
return false;
}
private static final String REF_SPEC_PATTERN = "+refs/heads/{0}:refs/remotes/{1}/{0}"; //NOI18N
private static final String REF_SPEC_GLOBAL_PATTERN = "+refs/heads/*:refs/remotes/{0}/*"; //NOI18N
public static final String REF_SPEC_DEL_PREFIX = ":refs/remotes/"; //NOI18N
private static final String REF_PUSHSPEC_PATTERN = "refs/heads/{0}:refs/heads/{1}"; //NOI18N
private static final String REF_PUSHSPEC_PATTERN_FORCE = "+refs/heads/{0}:refs/heads/{1}"; //NOI18N
public static final String REF_PUSHSPEC_DEL_PREFIX = ":refs/heads/"; //NOI18N
public static final String REF_PUSHSPEC_DEL_TAG_PREFIX = ":refs/tags/"; //NOI18N
private static final String REF_TAG_PUSHSPEC_PATTERN = "refs/tags/{0}:refs/tags/{0}"; //NOI18N
private static final String REF_TAG_PUSHSPEC_PATTERN_FORCE = "+" + REF_TAG_PUSHSPEC_PATTERN; //NOI18N
public static String getGlobalRefSpec (String remoteName) {
return MessageFormat.format(REF_SPEC_GLOBAL_PATTERN, remoteName);
}
public static String getRefSpec(GitBranch branch, String remoteName) {
return MessageFormat.format(REF_SPEC_PATTERN, branch.getName(), remoteName);
}
public static String getDeletedRefSpec (GitBranch branch) {
return REF_SPEC_DEL_PREFIX + branch.getName();
}
public static String getRefSpec (String branchName, String remoteName) {
return MessageFormat.format(REF_SPEC_PATTERN, branchName, remoteName);
}
public static String getPushRefSpec (String branchName, String remoteRepositoryBranchName, boolean forceUpdate) {
return MessageFormat.format(forceUpdate
? REF_PUSHSPEC_PATTERN_FORCE
: REF_PUSHSPEC_PATTERN, branchName, remoteRepositoryBranchName);
}
public static String getPushDeletedRefSpec (String remoteRepositoryBranchName) {
return REF_PUSHSPEC_DEL_PREFIX + remoteRepositoryBranchName;
}
public static String getPushDeletedTagRefSpec(String remoteRepositoryBranchName) {
return REF_PUSHSPEC_DEL_TAG_PREFIX + remoteRepositoryBranchName;
}
public static String getPushTagRefSpec (String tagName, boolean forceUpdate) {
return MessageFormat.format(forceUpdate
? REF_TAG_PUSHSPEC_PATTERN_FORCE
: REF_TAG_PUSHSPEC_PATTERN, tagName);
}
public static T runWithoutIndexing (Callable callable, List files) throws GitException {
return runWithoutIndexing(callable, files.toArray(new File[files.size()]));
}
static ThreadLocal> indexingFiles = new ThreadLocal>();
public static T runWithoutIndexing (Callable callable, File... files) throws GitException {
try {
Set recursiveRoots = indexingFiles.get();
if (recursiveRoots != null) {
assert indexingFilesSubtree(recursiveRoots, files)
: "Recursive call does not permit different roots: "
+ recursiveRoots + " vs. " + Arrays.asList(files);
return callable.call();
} else {
try {
if (Git.LOG.isLoggable(Level.FINER)) {
Git.LOG.log(Level.FINER, "Running block in indexing bridge: on {0}", Arrays.asList(files)); //NOI18N
}
indexingFiles.set(new HashSet(Arrays.asList(files)));
return IndexingBridge.getInstance().runWithoutIndexing(callable, files);
} finally {
indexingFiles.remove();
}
}
} catch (GitException ex) {
throw ex;
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new GitException("Cannot run without indexing due to: " + ex.getMessage(), ex); //NOI18N
}
}
public static String getColorString (Color c) {
return "#" + getHex(c.getRed()) + getHex(c.getGreen()) + getHex(c.getBlue()); //NOI18N
}
private static String getHex (int i) {
String hex = Integer.toHexString(i & 0x000000FF);
if (hex.length() == 1) {
hex = "0" + hex; //NOI18N
}
return hex;
}
private static boolean indexingFilesSubtree (Set recursiveRoots, File[] files) {
for (File f : files) {
if (!recursiveRoots.contains(f)) {
boolean contained = false;
for (File root : recursiveRoots) {
if (Utils.isAncestorOrEqual(root, f)) {
contained = true;
break;
}
}
if (!contained) {
return false;
}
}
}
return true;
}
public static GitRemoteConfig prepareConfig(GitRemoteConfig original, String remoteName, String remoteUri, List fetchRefSpecs) {
List remoteUris;
String username = new CredentialsCallback().getUsername(remoteUri, "");
GitURI uriToSave = null;
try {
uriToSave = new GitURI(remoteUri);
if (username != null && !username.isEmpty()) {
uriToSave = uriToSave.setUser(username);
}
remoteUri = uriToSave.toPrivateString();
} catch (URISyntaxException ex) {
Logger.getLogger(GitUtils.class.getName()).log(Level.INFO, null, ex);
}
if (original != null) {
remoteUris = new ArrayList<>(original.getUris());
boolean added = false;
for (ListIterator it = remoteUris.listIterator(); it.hasNext(); ) {
String oldUri = it.next();
// check the urls are the same, ommit username and password
if (equal(uriToSave, remoteUri, oldUri)) {
it.set(remoteUri);
added = true;
break;
}
}
if (!added) {
remoteUris.add(0, remoteUri);
}
} else {
remoteUris = Arrays.asList(remoteUri);
}
List refSpecs;
if (original != null) {
refSpecs = new LinkedList(original.getFetchRefSpecs());
if (!refSpecs.contains(GitUtils.getRefSpec("*", remoteName))) {
for (String refSpec : fetchRefSpecs) {
if (!refSpecs.contains(refSpec)) {
refSpecs.add(refSpec);
}
}
}
} else {
refSpecs = fetchRefSpecs;
}
return new GitRemoteConfig(remoteName, remoteUris,
original == null ? Collections.emptyList() : original.getPushUris(),
refSpecs,
original == null ? Collections.emptyList() : original.getPushRefSpecs());
}
private static boolean equal (GitURI uri, String uriString, String otherUriString) {
if (uri != null) {
try {
GitURI otherUri = new GitURI(otherUriString);
return otherUri.setUser(null).toString().equals(uri.setUser(null).toString());
} catch (URISyntaxException ex) {
Logger.getLogger(GitUtils.class.getName()).log(Level.INFO, null, ex);
}
return uri.toString().equals(otherUriString) || uriString.equals(otherUriString);
}
return uriString.equals(otherUriString);
}
/**
* Reads all remotes in a local repository's config and logs remote repository urls.
* Does this only once per a NB session and repository
* @param repositoryRoot root of the local repository
*/
public static void logRemoteRepositoryAccess (final File repositoryRoot) {
if (loggedRepositories.add(repositoryRoot)) {
Git.getInstance().getRequestProcessor(repositoryRoot).post(new Runnable() {
@Override
public void run () {
Set urls = new HashSet();
GitClient client = null;
try {
client = Git.getInstance().getClient(repositoryRoot);
Map cfgs = client.getRemotes(GitUtils.NULL_PROGRESS_MONITOR);
for (Map.Entry e : cfgs.entrySet()) {
GitRemoteConfig cfg = e.getValue();
for (List uris : Arrays.asList(cfg.getUris(), cfg.getPushUris())) {
if (!uris.isEmpty()) {
urls.addAll(uris);
}
}
}
} catch (GitException ex) {
// not interested
} finally {
if (client != null) {
client.release();
}
}
if (urls.isEmpty()) {
Utils.logVCSExternalRepository("GIT", null);
}
for (String url : urls) {
if (!url.trim().isEmpty()) {
Utils.logVCSExternalRepository("GIT", url);
}
}
}
});
}
}
public static QuickSearch attachQuickSearch (List items, JPanel panel, JList listComponent,
DefaultListModel model, SearchCallback searchCallback) {
final QuickSearchCallback callback = new QuickSearchCallback(items, listComponent, model, searchCallback);
final QuickSearch qs = QuickSearch.attach(panel, BorderLayout.SOUTH, callback);
qs.setAlwaysShown(true);
listComponent.addKeyListener(new KeyListener() {
@Override
public void keyTyped (KeyEvent e) {
qs.processKeyEvent(e);
}
@Override
public void keyPressed (KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER
|| e.getKeyCode() == KeyEvent.VK_ESCAPE && !callback.quickSearchActive) {
// leave events up to other components
} else {
qs.processKeyEvent(e);
}
}
@Override
public void keyReleased (KeyEvent e) {
qs.processKeyEvent(e);
}
});
return qs;
}
public static boolean isValidRefName (String refName) {
return JGitUtils.isValidRefName(refName);
}
public static boolean isValidTagName (String tagName) {
return isValidRefName(PREFIX_R_TAGS + tagName);
}
public static boolean isValidBranchName (String branchName) {
return isValidRefName(PREFIX_R_HEADS + branchName);
}
public static String normalizeBranchName (String refName) {
return JGitUtils.normalizeBranchName(refName);
}
public static VCSContext getContextForFile (final File root) {
return getContextForFiles(new File[] { root });
}
public static VCSContext getContextForFiles (final File[] roots) {
Node[] nodes = new Node[roots.length];
for (int i = 0; i < roots.length; ++i) {
final File root = roots[i];
nodes[i] = new AbstractNode(Children.LEAF, Lookups.fixed(root)) {
@Override
public String getName () {
return root.getName();
}
};
}
return VCSContext.forNodes(nodes);
}
public static interface SearchCallback {
public boolean contains (T item, String needle);
}
private static class QuickSearchCallback implements QuickSearch.Callback, ListSelectionListener {
private boolean quickSearchActive;
private int currentPosition;
private final List results;
private final List items;
private final JList component;
private final DefaultListModel model;
private final SearchCallback callback;
private boolean internal;
public QuickSearchCallback (List items, JList component, DefaultListModel model, SearchCallback callback) {
this.items = new ArrayList(items);
results = new ArrayList(items);
this.component = component;
this.model = model;
this.callback = callback;
this.currentPosition = component.getSelectedIndex();
component.addListSelectionListener(this);
}
@Override
public void quickSearchUpdate (String searchText) {
quickSearchActive = true;
T selected = items.get(0);
if (currentPosition > -1) {
selected = results.get(currentPosition);
}
results.clear();
results.addAll(items);
if (!searchText.isEmpty()) {
for (ListIterator it = results.listIterator(); it.hasNext(); ) {
T item = it.next();
if (!callback.contains(item, searchText)) {
it.remove();
}
}
}
currentPosition = results.indexOf(selected);
if (currentPosition == -1 && !results.isEmpty()) {
currentPosition = 0;
}
updateView();
}
@Override
public void showNextSelection (boolean forward) {
if (currentPosition != -1) {
currentPosition += forward ? 1 : -1;
if (currentPosition < 0) {
currentPosition = results.size() - 1;
} else if (currentPosition == results.size()) {
currentPosition = 0;
}
updateSelection();
}
}
@Override
public String findMaxPrefix (String prefix) {
return prefix;
}
@Override
public void quickSearchConfirmed () {
EventQueue.invokeLater(new Runnable() {
@Override
public void run () {
component.requestFocusInWindow();
}
});
}
@Override
public void quickSearchCanceled () {
quickSearchUpdate("");
quickSearchActive = false;
EventQueue.invokeLater(new Runnable() {
@Override
public void run () {
component.requestFocusInWindow();
}
});
}
private void updateView () {
internal = true;
try {
model.removeAllElements();
for (T r : results) {
model.addElement(r);
}
updateSelection();
} finally {
internal = false;
}
}
private void updateSelection () {
if (currentPosition > -1 && currentPosition < results.size()) {
T rev = results.get(currentPosition);
boolean oldInternal = internal;
internal = true;
try {
component.setSelectedValue(rev, true);
} finally {
internal = oldInternal;
}
}
}
@Override
public void valueChanged (ListSelectionEvent e) {
if (!internal && !e.getValueIsAdjusting()) {
currentPosition = component.getSelectedIndex();
}
}
}
private static class NullProgressMonitor extends ProgressMonitor {
@Override
public boolean isCanceled () {
return false;
}
@Override
public void started (String command) {
}
@Override
public void finished () {
}
@Override
public void preparationsFailed (String message) {
}
@Override
public void notifyError (String message) {
}
@Override
public void notifyWarning (String message) {
}
}
private GitUtils() {
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy