/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed 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.uberfire.java.nio.fs.jgit; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.Authenticator; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import com.jcraft.jsch.Session; import com.jcraft.jsch.UserInfo; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ListBranchCommand; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.internal.storage.file.WindowCache; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProviderUserInfo; import org.eclipse.jgit.transport.JschConfigSessionFactory; import org.eclipse.jgit.transport.OpenSshConfig; import org.eclipse.jgit.transport.PostReceiveHook; import org.eclipse.jgit.transport.PreReceiveHook; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.RepositoryResolver; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.uberfire.commons.async.DisposableExecutor; import org.uberfire.commons.async.SimpleAsyncExecutorService; import org.uberfire.commons.cluster.ClusterService; import org.uberfire.commons.config.ConfigProperties; import org.uberfire.commons.config.ConfigProperties.ConfigProperty; import org.uberfire.commons.data.Pair; import org.uberfire.commons.lifecycle.Disposable; import org.uberfire.commons.message.MessageType; import org.uberfire.java.nio.EncodingUtil; import org.uberfire.java.nio.IOException; import org.uberfire.java.nio.base.AbstractPath; import org.uberfire.java.nio.base.BasicFileAttributesImpl; import org.uberfire.java.nio.base.ExtendedAttributeView; import org.uberfire.java.nio.base.FileDiff; import org.uberfire.java.nio.base.FileSystemState; import org.uberfire.java.nio.base.SeekableByteChannelFileBasedImpl; import org.uberfire.java.nio.base.WatchContext; import org.uberfire.java.nio.base.attributes.HiddenAttributeView; import org.uberfire.java.nio.base.attributes.HiddenAttributes; import org.uberfire.java.nio.base.dotfiles.DotFileOption; import org.uberfire.java.nio.base.options.CherryPickCopyOption; import org.uberfire.java.nio.base.options.CommentedOption; import org.uberfire.java.nio.base.options.MergeCopyOption; import org.uberfire.java.nio.base.options.SquashOption; import org.uberfire.java.nio.base.version.VersionAttributeView; import org.uberfire.java.nio.base.version.VersionAttributes; import org.uberfire.java.nio.channels.AsynchronousFileChannel; import org.uberfire.java.nio.channels.SeekableByteChannel; import org.uberfire.java.nio.file.AccessDeniedException; import org.uberfire.java.nio.file.AccessMode; import org.uberfire.java.nio.file.AtomicMoveNotSupportedException; import org.uberfire.java.nio.file.CopyOption; import org.uberfire.java.nio.file.DeleteOption; import org.uberfire.java.nio.file.DirectoryNotEmptyException; import org.uberfire.java.nio.file.DirectoryStream; import org.uberfire.java.nio.file.FileAlreadyExistsException; import org.uberfire.java.nio.file.FileStore; import org.uberfire.java.nio.file.FileSystem; import org.uberfire.java.nio.file.FileSystemAlreadyExistsException; import org.uberfire.java.nio.file.FileSystemNotFoundException; import org.uberfire.java.nio.file.LinkOption; import org.uberfire.java.nio.file.NoSuchFileException; import org.uberfire.java.nio.file.NotDirectoryException; import org.uberfire.java.nio.file.NotLinkException; import org.uberfire.java.nio.file.OpenOption; import org.uberfire.java.nio.file.Option; import org.uberfire.java.nio.file.Path; import org.uberfire.java.nio.file.StandardCopyOption; import org.uberfire.java.nio.file.StandardDeleteOption; import org.uberfire.java.nio.file.StandardOpenOption; import org.uberfire.java.nio.file.StandardWatchEventKind; import org.uberfire.java.nio.file.WatchEvent; import org.uberfire.java.nio.file.attribute.BasicFileAttributeView; import org.uberfire.java.nio.file.attribute.BasicFileAttributes; import org.uberfire.java.nio.file.attribute.FileAttribute; import org.uberfire.java.nio.file.attribute.FileAttributeView; import org.uberfire.java.nio.fs.jgit.daemon.git.Daemon; import org.uberfire.java.nio.fs.jgit.daemon.git.DaemonClient; import org.uberfire.java.nio.fs.jgit.daemon.ssh.BaseGitCommand; import org.uberfire.java.nio.fs.jgit.daemon.ssh.GitSSHService; import org.uberfire.java.nio.fs.jgit.util.CommitContent; import org.uberfire.java.nio.fs.jgit.util.CopyCommitContent; import org.uberfire.java.nio.fs.jgit.util.DefaultCommitContent; import org.uberfire.java.nio.fs.jgit.util.JGitUtil; import org.uberfire.java.nio.fs.jgit.util.JGitUtil.JGitPathInfo; import org.uberfire.java.nio.fs.jgit.util.JGitUtil.PathType; import org.uberfire.java.nio.fs.jgit.util.MoveCommitContent; import org.uberfire.java.nio.fs.jgit.util.ProxyAuthenticator; import org.uberfire.java.nio.fs.jgit.util.RevertCommitContent; import org.uberfire.java.nio.fs.jgit.util.commands.DiffBranches; import org.uberfire.java.nio.fs.jgit.util.commands.Fork; import org.uberfire.java.nio.fs.jgit.util.commands.Merge; import org.uberfire.java.nio.fs.jgit.util.commands.Mirror; import org.uberfire.java.nio.fs.jgit.util.commands.Squash; import org.uberfire.java.nio.fs.jgit.util.exceptions.GitException; import org.uberfire.java.nio.security.FileSystemAuthenticator; import org.uberfire.java.nio.security.FileSystemAuthorizer; import org.uberfire.java.nio.security.SecuredFileSystemProvider; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT; import static org.uberfire.commons.validation.PortablePreconditions.checkCondition; import static org.uberfire.commons.validation.PortablePreconditions.checkNotEmpty; import static org.uberfire.commons.validation.PortablePreconditions.checkNotNull; import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.buildDotFile; import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.dot; import static org.uberfire.java.nio.file.StandardOpenOption.READ; import static org.uberfire.java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.PathType.DIRECTORY; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.PathType.NOT_FOUND; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.checkPath; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.cloneRepository; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.fixPath; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.getBranch; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.hasBranch; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.listPathContent; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.newRepository; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.pushRepository; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.resolveInputStream; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.resolvePath; import static org.uberfire.java.nio.fs.jgit.util.JGitUtil.syncRepository; public class JGitFileSystemProvider implements SecuredFileSystemProvider, Disposable { public static final String GIT_ENV_KEY_DEFAULT_REMOTE_NAME = DEFAULT_REMOTE_NAME; public static final String GIT_DAEMON_ENABLED = "org.uberfire.nio.git.daemon.enabled"; public static final String GIT_SSH_ENABLED = "org.uberfire.nio.git.ssh.enabled"; public static final String GIT_NIO_DIR = "org.uberfire.nio.git.dir"; public static final String GIT_NIO_DIR_NAME = "org.uberfire.nio.git.dirname"; /** * Specifies the list mode for the repository parent directory. Must match one of the enum constants defined in * {@link ListMode}. */ public static final String GIT_ENV_KEY_LIST_MODE = "listMode"; public static final String GIT_ENV_KEY_DEST_PATH = "out-dir"; public static final String GIT_ENV_KEY_USER_NAME = "username"; public static final String GIT_ENV_KEY_PASSWORD = "password"; public static final String GIT_ENV_KEY_INIT = "init"; public static final String REPOSITORIES_CONTAINER_DIR = ".niogit"; public static final String SSH_FILE_CERT_CONTAINER_DIR = ".security"; public static final String DEFAULT_HOST_NAME = "localhost"; public static final String DEFAULT_HOST_ADDR = "127.0.0.1"; public static final String DAEMON_DEFAULT_ENABLED = "true"; public static final String DAEMON_DEFAULT_PORT = "9418"; public static final String SSH_DEFAULT_ENABLED = "true"; public static final String SSH_DEFAULT_PORT = "8001"; public static final String SSH_IDLE_TIMEOUT = "10000"; public static final String SSH_ALGORITHM = "DSA"; public static final String SSH_CERT_PASSPHRASE = ""; public static final String DEFAULT_COMMIT_LIMIT_TO_GC = "20"; protected static final String DEFAULT_IO_SERVICE_NAME = "default"; private static final Logger LOG = LoggerFactory.getLogger(JGitFileSystemProvider.class); private static final String SCHEME = "git"; private static final int SCHEME_SIZE = (SCHEME + "://").length(); private static final int DEFAULT_SCHEME_SIZE = ("default://").length(); private static final String GIT_ENV_KEY_MIGRATE_FROM = "migrate-from"; private final Map<String, JGitFileSystem> fileSystems = new ConcurrentHashMap<String, JGitFileSystem>(); private final Set<JGitFileSystem> closedFileSystems = new HashSet<JGitFileSystem>(); private final Map<Repository, JGitFileSystem> repoIndex = new ConcurrentHashMap<Repository, JGitFileSystem>(); private final Map<Repository, ClusterService> clusterMap = new ConcurrentHashMap<Repository, ClusterService>(); private final Map<String, String> fullHostNames = new HashMap<String, String>(); private final Object oldHeadsOfPendingDiffsLock = new Object(); private final Map<JGitFileSystem, Map<String, NotificationModel>> oldHeadsOfPendingDiffs = new ConcurrentHashMap<JGitFileSystem, Map<String, NotificationModel>>(); private File gitReposParentDir; private File hookDir; private int commitLimit; private boolean daemonEnabled; private int daemonPort; private String daemonHostAddr; private String daemonHostName; private boolean sshEnabled; private int sshPort; private String sshHostAddr; private String sshHostName; private File sshFileCertDir; private String sshAlgorithm; private String sshPassphrase; private String sshIdleTimeout; private boolean isDefault; private Daemon daemonService = null; private GitSSHService gitSSHService = null; private FS detectedFS = FS.DETECTED; /** * Creates a JGit filesystem provider which takes its configuration from system properties. In a normal production * deployment of UberFire, this is the constructor that will be invoked by the ServiceLoader mechanism. * For a list of properties that affect the configuration of JGitFileSystemProvider, see the DEBUG log output of * this class during startup. */ public JGitFileSystemProvider() { this(new ConfigProperties(System.getProperties())); } /** * Creates a JGit filesystem provider which takes its configuration from the given map. * For a list of properties that affect the configuration of JGitFileSystemProvider, see the DEBUG log output of * this class during startup. */ public JGitFileSystemProvider(final Map<String, String> gitPrefs) { this(new ConfigProperties(gitPrefs)); } /** * Creates a JGit filesystem provider which takes its configuration from the given ConfigProperties instance. * For a list of properties that affect the configuration of JGitFileSystemProvider, see the DEBUG log output of * this class during startup. */ public JGitFileSystemProvider(final ConfigProperties gitPrefs) { loadConfig(gitPrefs); CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider("guest", "")); //Setup SSH authorization JschConfigSessionFactory sessionFactory = new JschConfigSessionFactory() { @Override protected void configure(final OpenSshConfig.Host hc, final Session session) { final CredentialsProvider provider = new CredentialsProvider() { @Override public boolean isInteractive() { return false; } @Override public boolean supports(final CredentialItem... items) { return true; } @Override public boolean get(final URIish uri, final CredentialItem... items) throws UnsupportedCredentialItem { for (CredentialItem item : items) { if (item instanceof CredentialItem.YesNoType) { ((CredentialItem.YesNoType) item).setValue(true); } else if (item instanceof CredentialItem.StringType) { ((CredentialItem.StringType) item).setValue(sshPassphrase); } } return true; } }; final UserInfo userInfo = new CredentialsProviderUserInfo(session, provider); session.setUserInfo(userInfo); } }; SshSessionFactory.setInstance(sessionFactory); //Setup daemon and service if (daemonEnabled) { fullHostNames.put("git", daemonHostName + ":" + daemonPort); } if (sshEnabled) { fullHostNames.put("ssh", sshHostName + ":" + sshPort); } rescanForExistingRepositories(); if (daemonEnabled) { buildAndStartDaemon(); } else { daemonService = null; } if (sshEnabled) { buildAndStartSSH(); } else { gitSSHService = null; } } //by spec, it should be a list of pairs, but here we're just uisng a map. private static Map<String, String> getQueryParams(final URI uri) { final String[] params = uri.getQuery().split("&"); return new HashMap<String, String>(params.length) {{ for (String param : params) { final String[] kv = param.split("="); final String name = kv[0]; final String value; if (kv.length == 2) { value = kv[1]; } else { value = ""; } put(name, value); } }}; } private void loadConfig(final ConfigProperties config) { LOG.debug("Configuring from properties:"); final String currentDirectory = System.getProperty("user.dir"); final ConfigProperty hookDirProp = config.get("org.uberfire.nio.git.hooks", null); final ConfigProperty bareReposDirProp = config.get(GIT_NIO_DIR, currentDirectory); final ConfigProperty reposDirNameProp = config.get(GIT_NIO_DIR_NAME, REPOSITORIES_CONTAINER_DIR); final ConfigProperty enabledProp = config.get(GIT_DAEMON_ENABLED, DAEMON_DEFAULT_ENABLED); final ConfigProperty hostProp = config.get("org.uberfire.nio.git.daemon.host", DEFAULT_HOST_ADDR); final ConfigProperty hostNameProp = config.get("org.uberfire.nio.git.daemon.hostname", hostProp.isDefault() ? DEFAULT_HOST_NAME : hostProp.getValue()); final ConfigProperty portProp = config.get("org.uberfire.nio.git.daemon.port", DAEMON_DEFAULT_PORT); final ConfigProperty sshEnabledProp = config.get(GIT_SSH_ENABLED, SSH_DEFAULT_ENABLED); final ConfigProperty sshHostProp = config.get("org.uberfire.nio.git.ssh.host", DEFAULT_HOST_ADDR); final ConfigProperty sshHostNameProp = config.get("org.uberfire.nio.git.ssh.hostname", sshHostProp.isDefault() ? DEFAULT_HOST_NAME : sshHostProp.getValue()); final ConfigProperty sshPortProp = config.get("org.uberfire.nio.git.ssh.port", SSH_DEFAULT_PORT); final ConfigProperty sshCertDirProp = config.get("org.uberfire.nio.git.ssh.cert.dir", currentDirectory); final ConfigProperty sshIdleTimeoutProp = config.get("org.uberfire.nio.git.ssh.idle.timeout", SSH_IDLE_TIMEOUT); final ConfigProperty sshAlgorithmProp = config.get("org.uberfire.nio.git.ssh.algorithm", SSH_ALGORITHM); final ConfigProperty sshPassphraseProp = config.get("org.uberfire.nio.git.ssh.passphrase", SSH_CERT_PASSPHRASE); final ConfigProperty commitLimitProp = config.get("org.uberfire.nio.git.gc.limit", DEFAULT_COMMIT_LIMIT_TO_GC); final ConfigProperty httpProxyUserProp = config.get("http.proxyUser", null); final ConfigProperty httpProxyPasswordProp = config.get("http.proxyPassword", null); final ConfigProperty httpsProxyUserProp = config.get("https.proxyUser", null); final ConfigProperty httpsProxyPasswordProp = config.get("https.proxyPassword", null); if (LOG.isDebugEnabled()) { LOG.debug(config.getConfigurationSummary("Summary of JGit configuration:")); } if (hookDirProp != null && hookDirProp.getValue() != null) { hookDir = new File(hookDirProp.getValue()); if (!hookDir.exists()) { hookDir = null; } } gitReposParentDir = new File(bareReposDirProp.getValue(), reposDirNameProp.getValue()); commitLimit = commitLimitProp.getIntValue(); daemonEnabled = enabledProp.getBooleanValue(); if (daemonEnabled) { daemonPort = portProp.getIntValue(); daemonHostAddr = hostProp.getValue(); daemonHostName = hostNameProp.getValue(); } sshEnabled = sshEnabledProp.getBooleanValue(); if (sshEnabled) { sshPort = sshPortProp.getIntValue(); sshHostAddr = sshHostProp.getValue(); sshHostName = sshHostNameProp.getValue(); sshFileCertDir = new File(sshCertDirProp.getValue(), SSH_FILE_CERT_CONTAINER_DIR); sshAlgorithm = sshAlgorithmProp.getValue(); sshIdleTimeout = sshIdleTimeoutProp.getValue(); try { Integer.valueOf(sshIdleTimeout); } catch (final NumberFormatException exception) { LOG.error("SSH Idle Timeout value is not a valid integer - Parameter is ignored, now using default value."); sshIdleTimeout = SSH_IDLE_TIMEOUT; } } sshPassphrase = sshPassphraseProp.getValue(); if ((httpProxyUserProp.getValue() != null && httpProxyPasswordProp.getValue() != null) || (httpsProxyUserProp.getValue() != null && httpsProxyPasswordProp.getValue() != null)) { setupProxyAuthentication(httpProxyUserProp.getValue(), httpProxyPasswordProp.getValue(), httpsProxyUserProp.getValue(), httpsProxyPasswordProp.getValue()); } } private void setupProxyAuthentication(final String httpProxyUser, final String httpProxyPassword, final String httpsProxyUser, final String httpsProxyPassword) { Authenticator.setDefault(new ProxyAuthenticator(httpProxyUser, httpProxyPassword, httpsProxyUser, httpsProxyPassword)); } public void onCloseFileSystem(final JGitFileSystem fileSystem) { closedFileSystems.add(fileSystem); synchronized (oldHeadsOfPendingDiffsLock) { oldHeadsOfPendingDiffs.remove(fileSystem); } if (closedFileSystems.size() == fileSystems.size()) { forceStopDaemon(); shutdownSSH(); } } public void onDisposeFileSystem(final JGitFileSystem fileSystem) { onCloseFileSystem(fileSystem); closedFileSystems.remove(fileSystem); fileSystems.remove(fileSystem.id()); repoIndex.remove(fileSystem.gitRepo().getRepository()); clusterMap.remove(fileSystem.gitRepo().getRepository()); } public Set<JGitFileSystem> getOpenFileSystems() { Set<JGitFileSystem> open = new HashSet<JGitFileSystem>(fileSystems.values()); open.removeAll(closedFileSystems); return open; } @Override public void setAuthenticator(final FileSystemAuthenticator authenticator) { checkNotNull("authenticator", authenticator); if (gitSSHService != null) { gitSSHService.setUserPassAuthenticator(authenticator); } } @Override public void setAuthorizer(FileSystemAuthorizer authorizer) { checkNotNull("authorizer", authorizer); if (gitSSHService != null) { gitSSHService.setAuthorizationManager(authorizer); } } @Override public void dispose() { shutdown(); } /** * Forgets all existing registered filesystems and scans for existing git repositories under * {@link #gitReposParentDir}. Call this method any time you add or remove git repositories without using this * class. If you only ever add or remove git repositories using the methods of this class, there is no need to call * this method. */ public final void rescanForExistingRepositories() { fileSystems.clear(); final List<Pair<String, String>> repos = getRepositories(gitReposParentDir); if (repos != null) { for (Pair<String, String> repo : repos) { final File repoDir = new File(gitReposParentDir, repo.getK1() + repo.getK2()); try { if (repoDir.isDirectory()) { // final String name = repoDir.getName().substring( 0, repoDir.getName().indexOf( DOT_GIT_EXT ) ); final String name = repo.getK1() + repo.getK2().substring(0, repo.getK2().indexOf(DOT_GIT_EXT)); //Default to ListMode of null to avoid indexing scanning remote branches. Ideally the ListMode should //be identical to that used when the original JGitFileSystem was created however that information is not //persisted. Using a default of null rather than ALL is a safer default as *all* GIT repositories created //from within the workbench have a ListMode of null. final JGitFileSystem fs = new JGitFileSystem(this, fullHostNames, newRepository(repoDir, true), name, null, buildCredential(null)); LOG.debug("Running GIT GC on '" + name + "'"); JGitUtil.gc(fs.gitRepo()); LOG.debug("Registering existing GIT filesystem '" + name + "' at " + repoDir); fileSystems.put(name, fs); repoIndex.put(fs.gitRepo().getRepository(), fs); } else { LOG.debug("Not registering " + repoDir + " as a GIT filesystem because it is not a directory"); } } catch (final Exception ex) { LOG.error("Not registering " + repoDir + " as a GIT filesystem failed", ex); } } } } private List<Pair<String, String>> getRepositories(File root) { List<Pair<String, String>> repositories = new ArrayList<>(); final String[] topLevelRepositories = root.list((file, s) -> s.endsWith(DOT_GIT_EXT)); if (topLevelRepositories != null) { List<Pair<String, String>> repos = Arrays .asList(topLevelRepositories) .stream() .map(dir -> Pair.newPair("", dir)).collect(Collectors.toList()); repositories.addAll(repos); } final String[] topLevelFolders = root.list((file, s) -> { return !s.endsWith(DOT_GIT_EXT); }); if (topLevelFolders != null) { Arrays .asList(topLevelFolders) .stream() .forEach((dir) -> { final File subRoot = new File(root.getPath() + "/" + dir); final String[] repos = subRoot.list((file, name) -> name.endsWith(DOT_GIT_EXT)); if (repos != null) { Arrays.asList(repos) .stream() .forEach((repo) -> { repositories.add(Pair.newPair(dir + "/", repo)); }); } } ); } return repositories; } private void buildAndStartSSH() { final ReceivePackFactory receivePackFactory = new ReceivePackFactory<BaseGitCommand>() { @Override public ReceivePack create(final BaseGitCommand req, final Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { return new ReceivePack(db) {{ final ClusterService clusterService = clusterMap.get(db); final JGitFileSystem fs = repoIndex.get(db); final Map<String, RevCommit> oldTreeRefs = new HashMap<String, RevCommit>(); setPreReceiveHook(new PreReceiveHook() { @Override public void onPreReceive(final ReceivePack rp, final Collection<ReceiveCommand> commands) { fs.lock(); if (clusterService != null) { clusterService.lock(); } for (final ReceiveCommand command : commands) { final RevCommit lastCommit = JGitUtil.getLastCommit(fs.gitRepo(), command.getRefName()); oldTreeRefs.put(command.getRefName(), lastCommit); } } }); setPostReceiveHook(new PostReceiveHook() { @Override public void onPostReceive(final ReceivePack rp, final Collection<ReceiveCommand> commands) { fs.unlock(); final String userName = req.getUser().getName(); for (Map.Entry<String, RevCommit> oldTreeRef : oldTreeRefs.entrySet()) { final List<RevCommit> commits = JGitUtil.getCommits(fs.gitRepo(), oldTreeRef.getKey(), oldTreeRef.getValue(), JGitUtil.getLastCommit(fs.gitRepo(), oldTreeRef.getKey())); for (final RevCommit revCommit : commits) { final RevTree parent = revCommit.getParentCount() > 0 ? revCommit.getParent(0).getTree() : null; notifyDiffs(fs, oldTreeRef.getKey(), "<ssh>", userName, revCommit.getFullMessage(), parent, revCommit.getTree()); } } if (clusterService != null) { //TODO {porcelli} hack, that should be addressed in future clusterService.broadcast(DEFAULT_IO_SERVICE_NAME, new MessageType() { @Override public String toString() { return "SYNC_FS"; } @Override public int hashCode() { return "SYNC_FS".hashCode(); } }, new HashMap<String, String>() {{ put("fs_scheme", "git"); put("fs_id", fs.id()); put("fs_uri", fs.toString()); }} ); clusterService.unlock(); } } }); }}; } }; gitSSHService = new GitSSHService(); gitSSHService.setup(sshFileCertDir, InetSocketAddress.createUnresolved(sshHostAddr, sshPort), sshIdleTimeout, sshAlgorithm, receivePackFactory, new RepositoryResolverImpl<BaseGitCommand>()); gitSSHService.start(); } void buildAndStartDaemon() { if (daemonService == null || !daemonService.isRunning()) { daemonService = new Daemon(new InetSocketAddress(daemonHostAddr, daemonPort), new ExecutorWrapper(SimpleAsyncExecutorService.getUnmanagedInstance())); daemonService.setRepositoryResolver(new RepositoryResolverImpl<DaemonClient>()); try { daemonService.start(); } catch (java.io.IOException e) { throw new IOException(e); } } } private void shutdownSSH() { if (gitSSHService != null) { gitSSHService.stop(); } } void forceStopDaemon() { if (daemonService != null && daemonService.isRunning()) { daemonService.stop(); } } /** * Closes and disposes all open filesystems and stops the Git and SSH daemons if they are running. This filesystem * provider can be reactivated by attempting to open a new filesystem, by creating a new filesystem, or by calling * {@link #rescanForExistingRepositories()}. */ public void shutdown() { for (JGitFileSystem fs : getOpenFileSystems()) { fs.close(); } shutdownSSH(); forceStopDaemon(); } /** * Returns the directory that contains all the git repositories managed by this file system provider. */ public File getGitRepoContainerDir() { return gitReposParentDir; } @Override public synchronized void forceAsDefault() { this.isDefault = true; } @Override public boolean isDefault() { return isDefault; } @Override public String getScheme() { return SCHEME; } @Override public FileSystem newFileSystem(final Path path, final Map<String, ?> env) throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException { throw new UnsupportedOperationException(); } @Override public FileSystem newFileSystem(final URI uri, final Map<String, ?> env) throws IllegalArgumentException, IOException, SecurityException, FileSystemAlreadyExistsException { checkNotNull("uri", uri); checkCondition("uri scheme not supported", uri.getScheme().equals(getScheme()) || uri.getScheme().equals("default")); checkURI("uri", uri); checkNotNull("env", env); String name = extractRepoName(uri); migrateIfNeeded(env, name); if (fileSystems.containsKey(name)) { throw new FileSystemAlreadyExistsException("No filesystem for uri (" + uri + ") found."); } ListBranchCommand.ListMode listMode; if (env.containsKey(GIT_ENV_KEY_LIST_MODE)) { try { listMode = ListBranchCommand.ListMode.valueOf((String) env.get(GIT_ENV_KEY_LIST_MODE)); } catch (Exception ex) { listMode = null; } } else { listMode = null; } final Git git; final CredentialsProvider credential; boolean bare = true; final String outPath = (String) env.get(GIT_ENV_KEY_DEST_PATH); final File repoDest; if (outPath != null) { repoDest = new File(outPath, name + DOT_GIT_EXT); } else { repoDest = new File(gitReposParentDir, name + DOT_GIT_EXT); } if (env.containsKey(GIT_ENV_KEY_DEFAULT_REMOTE_NAME)) { final String originURI = env.get(GIT_ENV_KEY_DEFAULT_REMOTE_NAME).toString(); credential = buildCredential(env); if (this.isForkOrigin(originURI)) { git = new Fork(this.getGitRepoContainerDir(), originURI, name, credential).execute().get(); } else { new Mirror(repoDest, originURI, credential).execute(); git = cloneRepository(repoDest, originURI, bare, credential); } } else { credential = buildCredential(null); git = newRepository(repoDest, bare, hookDir); } final JGitFileSystem fs = new JGitFileSystem(this, fullHostNames, git, name, listMode, credential); fileSystems.put(name, fs); repoIndex.put(fs.gitRepo().getRepository(), fs); boolean init = false; if (env.containsKey(GIT_ENV_KEY_INIT) && Boolean.valueOf(env.get(GIT_ENV_KEY_INIT).toString())) { init = true; } if (!env.containsKey(GIT_ENV_KEY_DEFAULT_REMOTE_NAME) && init) { try { final URI initURI = URI.create(getScheme() + "://master@" + name + "/readme.md"); final CommentedOption op = setupOp(env); final OutputStream stream = newOutputStream(getPath(initURI), op); final String _init = "Repository Init Content\n" + "=======================\n" + "\n" + "Your project description here."; stream.write(_init.getBytes()); stream.close(); } catch (final Exception e) { } if (!bare) { //todo: checkout } } final Object _clusterService = env.get("clusterService"); if (_clusterService != null && _clusterService instanceof ClusterService) { clusterMap.put(git.getRepository(), (ClusterService) _clusterService); } if (daemonEnabled && daemonService != null && !daemonService.isRunning()) { buildAndStartDaemon(); } return fs; } private void migrateIfNeeded(final Map<String, ?> env, final String name) { if (env.containsKey(GIT_ENV_KEY_MIGRATE_FROM)) { URI migrateFromURI = (URI) env.get(GIT_ENV_KEY_MIGRATE_FROM); final String oldRepoName = extractRepoName(migrateFromURI); if (fileSystems.containsKey(oldRepoName) && !fileSystems.containsKey(name)) { this.migrateOldRepository(oldRepoName, name); } } } private String extractRepoName(final URI uri) { String authority = uri.getAuthority(); String path = uri.getPath(); String name = authority + path; if (path == null || "/".equals(path)) { name = authority; } return name; } private boolean isForkOrigin(final String originURI) { return originURI.matches("(^\\w+\\/\\w+$)"); } private void migrateOldRepository(String oldName, String newName) { final File oldRepository = new File(this.getGitRepoContainerDir(), oldName + DOT_GIT_EXT); final File newRepository = new File(this.getGitRepoContainerDir(), newName + DOT_GIT_EXT); if (oldRepository.exists()) { if (!newRepository.exists()) { try { Files.createDirectories(newRepository.toPath()); Files.move(oldRepository.toPath(), newRepository.toPath(), REPLACE_EXISTING); this.fileSystems.remove(oldName); } catch (java.io.IOException e) { throw new GitException("A problem occurred trying to migrate repositories", e); } } } } private Optional<Pair<String, String>> extractOldName(final String name) { if (name.contains("/")) { String oldName = name.substring(name.indexOf("/") + 1); return Optional.of(new Pair<String, String>(name, oldName)); } else { return Optional.empty(); } } private String getRepoName(final URI uri) { String path = uri.getPath(); int indexOf = path.lastIndexOf("/"); return path.substring(indexOf + 1); } private CommentedOption setupOp(final Map<String, ?> env) { return null; } @Override public FileSystem getFileSystem(final URI uri) throws IllegalArgumentException, FileSystemNotFoundException, SecurityException { checkNotNull("uri", uri); checkCondition("uri scheme not supported", uri.getScheme().equals(getScheme()) || uri.getScheme().equals("default")); checkURI("uri", uri); JGitFileSystem fileSystem = fileSystems.get(extractRepoNameWithFolder(uri)); if (fileSystem == null) { fileSystem = fileSystems.get(extractRepoNameWithoutFolder(uri)); if (fileSystem == null) { throw new FileSystemNotFoundException("No filesystem for uri (" + uri + ") found."); } } if (hasSyncFlag(uri)) { try { final String treeRef = "master"; final ObjectId oldHead = JGitUtil.getTreeRefObjectId(fileSystem.gitRepo().getRepository(), treeRef); final Map<String, String> params = getQueryParams(uri); try { fileSystem.lock(); syncRepository(fileSystem.gitRepo(), fileSystem.getCredential(), params.get("sync"), hasForceFlag(uri)); } finally { fileSystem.unlock(); } final ObjectId newHead = JGitUtil.getTreeRefObjectId(fileSystem.gitRepo().getRepository(), treeRef); notifyDiffs(fileSystem, treeRef, "<system>", "<system>", "", oldHead, newHead); } catch (final Exception ex) { throw new IOException("Failed to sync repository.", ex); } } if (hasPushFlag(uri)) { try { final Map<String, String> params = getQueryParams(uri); pushRepository(fileSystem.gitRepo(), fileSystem.getCredential(), params.get("push"), hasForceFlag(uri)); } catch (final Exception ex) { throw new IOException("Failed to push repository.", ex); } } return fileSystem; } @Override public Path getPath(final URI uri) throws IllegalArgumentException, FileSystemNotFoundException, SecurityException { checkNotNull("uri", uri); checkCondition("uri scheme not supported", uri.getScheme().equals(getScheme()) || uri.getScheme().equals("default")); checkURI("uri", uri); Path path; JGitFileSystem fileSystem = fileSystems.get(extractRepoNameWithFolder(uri)); if (fileSystem == null) { fileSystem = fileSystems.get(extractRepoNameWithoutFolder(uri)); if (fileSystem == null) { throw new FileSystemNotFoundException("No filesystem for uri (" + uri + ") found."); } else { path = JGitPathImpl.create(fileSystem, extractOldPath(uri), extractOldHost(uri), false); } } else { path = JGitPathImpl.create(fileSystem, extractPath(uri), extractHost(uri), false); } return path; } @Override public InputStream newInputStream(final Path path, final OpenOption... options) throws IllegalArgumentException, UnsupportedOperationException, NoSuchFileException, IOException, SecurityException { checkNotNull("path", path); final JGitPathImpl gPath = toPathImpl(path); return resolveInputStream(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); } @Override public OutputStream newOutputStream(final Path path, final OpenOption... options) throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException { checkNotNull("path", path); final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> result = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (result.getK1().equals(PathType.DIRECTORY)) { throw new NotDirectoryException(path.toString()); } try { final File file = File.createTempFile("gitz", "woot"); return new FilterOutputStream(new FileOutputStream(file)) { @Override public void close() throws java.io.IOException { super.close(); commit(gPath, buildCommitInfo("{" + toPathImpl(path).getPath() + "}", Arrays.asList(options)), new DefaultCommitContent(new HashMap<String, File>() {{ put(gPath.getPath(), file); }})); } }; } catch (java.io.IOException e) { throw new IOException("Could not create file or output stream.", e); } } private CommitInfo buildCommitInfo(final String defaultMessage, final Collection<? extends Option> options) { String sessionId = null; String name = null; String email = null; String message = defaultMessage; TimeZone timeZone = null; Date when = null; if (options != null && !options.isEmpty()) { final CommentedOption op = extractCommentedOption(options); if (op != null) { sessionId = op.getSessionId(); name = op.getName(); email = op.getEmail(); if (op.getMessage() != null && !op.getMessage().trim().isEmpty()) { message = op.getMessage() + " " + defaultMessage; } timeZone = op.getTimeZone(); when = op.getWhen(); } } return new CommitInfo(sessionId, name, email, message, timeZone, when); } final CommentedOption extractCommentedOption(final Collection<? extends Option> options) { for (final Option option : options) { if (option instanceof CommentedOption) { return (CommentedOption) option; } } return null; } @Override public FileChannel newFileChannel(final Path path, Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException { throw new UnsupportedOperationException(); } @Override public AsynchronousFileChannel newAsynchronousFileChannel(final Path path, final Set<? extends OpenOption> options, final ExecutorService executor, FileAttribute<?>... attrs) throws IllegalArgumentException, UnsupportedOperationException, IOException, SecurityException { throw new UnsupportedOperationException(); } @Override public SeekableByteChannel newByteChannel(final Path path, final Set<? extends OpenOption> options, final FileAttribute<?>... attrs) throws IllegalArgumentException, UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException { final JGitPathImpl gPath = toPathImpl(path); if (exists(path)) { if (!shouldCreateOrOpenAByteChannel(options)) { throw new FileAlreadyExistsException(path.toString()); } } final Pair<PathType, ObjectId> result = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (result.getK1().equals(PathType.DIRECTORY)) { throw new NotDirectoryException(path.toString()); } try { if (options != null && options.contains(READ)) { return openAByteChannel(path); } else { return createANewByteChannel(path, options, gPath, attrs); } } catch (java.io.IOException e) { throw new IOException("Failed to open or create a byte channel.", e); } finally { ((AbstractPath) path).clearCache(); } } private SeekableByteChannel createANewByteChannel(final Path path, final Set<? extends OpenOption> options, final JGitPathImpl gPath, final FileAttribute<?>[] attrs) throws java.io.IOException { final File file = File.createTempFile("gitz", "woot"); return new SeekableByteChannelFileBasedImpl(new RandomAccessFile(file, "rw").getChannel()) { @Override public void close() throws java.io.IOException { super.close(); File tempDot = null; final boolean hasDotContent; if (options != null && options.contains(new DotFileOption())) { deleteIfExists(dot(path), extractCommentedOption(options)); tempDot = File.createTempFile("meta", "dot"); hasDotContent = buildDotFile(path, new FileOutputStream(tempDot), attrs); } else { hasDotContent = false; } final File dotfile = tempDot; commit(gPath, buildCommitInfo("{" + toPathImpl(path).getPath() + "}", options), new DefaultCommitContent(new HashMap<String, File>() {{ put(gPath.getPath(), file); if (hasDotContent) { put(toPathImpl(dot(gPath)).getPath(), dotfile); } }})); } }; } private SeekableByteChannelFileBasedImpl openAByteChannel(Path path) throws FileNotFoundException { return new SeekableByteChannelFileBasedImpl(new RandomAccessFile(path.toFile(), "r").getChannel()); } private boolean shouldCreateOrOpenAByteChannel(Set<? extends OpenOption> options) { return (options != null && (options.contains(TRUNCATE_EXISTING) || options.contains(READ))); } protected boolean exists(final Path path) { try { readAttributes(path, BasicFileAttributes.class); return true; } catch (final Exception ignored) { // this means the file does not exist } return false; } @Override public DirectoryStream<Path> newDirectoryStream(final Path path, final DirectoryStream.Filter<Path> pfilter) throws NotDirectoryException, IOException, SecurityException { checkNotNull("path", path); final DirectoryStream.Filter<Path> filter; if (pfilter == null) { filter = new DirectoryStream.Filter<Path>() { @Override public boolean accept(final Path entry) throws IOException { return true; } }; } else { filter = pfilter; } final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> result = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (!result.getK1().equals(PathType.DIRECTORY)) { throw new NotDirectoryException(path.toString()); } final List<JGitPathInfo> pathContent = listPathContent(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); return new DirectoryStream<Path>() { boolean isClosed = false; @Override public void close() throws IOException { if (isClosed) { throw new IOException("This stream is closed."); } isClosed = true; } @Override public Iterator<Path> iterator() { if (isClosed) { throw new IOException("This stream is closed."); } return new Iterator<Path>() { public boolean atEof = false; private int i = -1; private Path nextEntry = null; @Override public boolean hasNext() { if (nextEntry == null && !atEof) { nextEntry = readNextEntry(); } return nextEntry != null; } @Override public Path next() { final Path result; if (nextEntry == null && !atEof) { result = readNextEntry(); } else { result = nextEntry; nextEntry = null; } if (result == null) { throw new NoSuchElementException(); } return result; } private Path readNextEntry() { if (atEof) { return null; } Path result = null; while (true) { i++; if (i >= pathContent.size()) { atEof = true; break; } final JGitPathInfo content = pathContent.get(i); final Path path = JGitPathImpl.create(gPath.getFileSystem(), "/" + content.getPath(), gPath.getHost(), content.getObjectId(), gPath.isRealPath()); if (filter.accept(path)) { result = path; break; } } return result; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } @Override public void createDirectory(final Path path, final FileAttribute<?>... attrs) throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException { checkNotNull("path", path); final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> result = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (!result.getK1().equals(NOT_FOUND)) { throw new FileAlreadyExistsException(path.toString()); } try { final OutputStream outputStream = newOutputStream(path.resolve(".gitkeep")); outputStream.close(); } catch (final Exception e) { throw new IOException("Failed to write to or close the output stream.", e); } } @Override public void createSymbolicLink(final Path link, final Path target, final FileAttribute<?>... attrs) throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException { throw new UnsupportedOperationException(); } @Override public void createLink(final Path link, final Path existing) throws UnsupportedOperationException, FileAlreadyExistsException, IOException, SecurityException { throw new UnsupportedOperationException(); } @Override public void delete(final Path path, final DeleteOption... options) throws DirectoryNotEmptyException, NoSuchFileException, IOException, SecurityException { checkNotNull("path", path); if (path instanceof JGitFSPath) { deleteRepo(path.getFileSystem()); return; } final JGitPathImpl gPath = toPathImpl(path); if (isBranch(gPath)) { deleteBranch(gPath); return; } deleteAsset(gPath, options); } private boolean deleteRepo(final FileSystem fileSystem) { final File gitDir = ((JGitFileSystem) fileSystem).gitRepo().getRepository().getDirectory(); fileSystem.close(); fileSystem.dispose(); try { if (System.getProperty("os.name").toLowerCase().contains("windows")) { //this operation forces a cache clean freeing any lock -> windows only issue! WindowCache.reconfigure(new WindowCacheConfig()); } FileUtils.delete(gitDir, FileUtils.RECURSIVE | FileUtils.RETRY); return true; } catch (java.io.IOException e) { throw new IOException("Failed to remove the git repository.", e); } } public void deleteAsset(final JGitPathImpl path, final DeleteOption... options) { final Pair<PathType, ObjectId> result = checkPath(path.getFileSystem().gitRepo(), path.getRefTree(), path.getPath()); if (result.getK1().equals(PathType.DIRECTORY)) { if (deleteNonEmptyDirectory(options)) { deleteResource(path, options); return; } final List<JGitPathInfo> content = listPathContent(path.getFileSystem().gitRepo(), path.getRefTree(), path.getPath()); if (content.size() == 1 && content.get(0).getPath().equals(path.getPath().substring(1) + "/.gitkeep")) { delete(path.resolve(".gitkeep")); deleteResource(path, options); return; } throw new DirectoryNotEmptyException(path.toString()); } if (result.getK1().equals(NOT_FOUND)) { throw new NoSuchFileException(path.toString()); } deleteResource(path, options); } void deleteResource(final JGitPathImpl path, final DeleteOption... options) { delete(path, buildCommitInfo("delete {" + path.getPath() + "}", Arrays.asList(options))); } private boolean deleteNonEmptyDirectory(final DeleteOption... options) { for (final DeleteOption option : options) { if (option.equals(StandardDeleteOption.NON_EMPTY_DIRECTORIES)) { return true; } } return false; } public void deleteBranch(final JGitPathImpl path) { final Ref branch = getBranch(path.getFileSystem().gitRepo(), path.getRefTree()); if (branch == null) { throw new NoSuchFileException(path.toString()); } try { path.getFileSystem().lock(); JGitUtil.deleteBranch(path.getFileSystem().gitRepo(), branch); } finally { path.getFileSystem().unlock(); } } @Override public boolean deleteIfExists(final Path path, final DeleteOption... options) throws DirectoryNotEmptyException, IOException, SecurityException { checkNotNull("path", path); if (path instanceof JGitFSPath) { return deleteRepo(path.getFileSystem()); } final JGitPathImpl gPath = toPathImpl(path); if (isBranch(gPath)) { return deleteBranchIfExists(gPath); } return deleteAssetIfExists(gPath, options); } public boolean deleteBranchIfExists(final JGitPathImpl path) { final Ref branch = getBranch(path.getFileSystem().gitRepo(), path.getRefTree()); if (branch == null) { return false; } try { path.getFileSystem().lock(); JGitUtil.deleteBranch(path.getFileSystem().gitRepo(), branch); } finally { path.getFileSystem().unlock(); } return true; } public boolean deleteAssetIfExists(final JGitPathImpl path, final DeleteOption... options) { final Pair<PathType, ObjectId> result = checkPath(path.getFileSystem().gitRepo(), path.getRefTree(), path.getPath()); if (result.getK1().equals(PathType.DIRECTORY)) { if (deleteNonEmptyDirectory(options)) { deleteResource(path, options); return true; } final List<JGitPathInfo> content = listPathContent(path.getFileSystem().gitRepo(), path.getRefTree(), path.getPath()); if (content.size() == 1 && content.get(0).getPath().equals(path.getPath().substring(1) + "/.gitkeep")) { delete(path.resolve(".gitkeep")); return true; } throw new DirectoryNotEmptyException(path.toString()); } if (result.getK1().equals(NOT_FOUND)) { return false; } deleteResource(path, options); return true; } @Override public Path readSymbolicLink(final Path link) throws UnsupportedOperationException, NotLinkException, IOException, SecurityException { checkNotNull("link", link); throw new UnsupportedOperationException(); } @Override public void copy(final Path source, final Path target, final CopyOption... options) throws UnsupportedOperationException, FileAlreadyExistsException, DirectoryNotEmptyException, IOException, SecurityException { checkNotNull("source", source); checkNotNull("target", target); final JGitPathImpl gSource = toPathImpl(source); final JGitPathImpl gTarget = toPathImpl(target); final boolean isBranch = isBranch(gSource) && isBranch(gTarget); if (options.length == 1 && options[0] instanceof MergeCopyOption) { if (!isBranch) { throw new IOException("Merge needs source and target as root."); } this.merge(gSource, gTarget); } else if (options.length == 1 && options[0] instanceof CherryPickCopyOption) { if (!isBranch) { throw new IOException("Cherry pick needs source and target as root."); } final String[] commits = ((CherryPickCopyOption) options[0]).getCommits(); if (commits == null || commits.length == 0) { throw new IOException("Cherry pick needs at least one commit id."); } cherryPick(gSource, gTarget, commits); } else { if (isBranch) { copyBranch(gSource, gTarget); return; } copyAsset(gSource, gTarget, options); } } private void merge(final JGitPathImpl source, final JGitPathImpl target) { try { target.getFileSystem().lock(); final Git repo = source.getFileSystem().gitRepo(); new Merge(source.getFileSystem().gitRepo(), source.getRefTree(), target.getRefTree()).execute(); } finally { target.getFileSystem().unlock(); } } private void cherryPick(final JGitPathImpl source, final JGitPathImpl target, final String... commits) { try { target.getFileSystem().lock(); JGitUtil.cherryPick(source.getFileSystem().gitRepo().getRepository(), target.getRefTree(), commits); } finally { target.getFileSystem().unlock(); } } private void copyBranch(final JGitPathImpl source, final JGitPathImpl target) { checkCondition("source and taget should have same setup", !hasSameFileSystem(source, target)); if (existsBranch(target)) { throw new FileAlreadyExistsException(target.toString()); } if (!existsBranch(source)) { throw new NoSuchFileException(target.toString()); } createBranch(source, target); } private void copyAsset(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final Pair<PathType, ObjectId> sourceResult = checkPath(source.getFileSystem().gitRepo(), source.getRefTree(), source.getPath()); final Pair<PathType, ObjectId> targetResult = checkPath(target.getFileSystem().gitRepo(), target.getRefTree(), target.getPath()); if (!isRoot(target) && targetResult.getK1() != NOT_FOUND) { if (!contains(options, StandardCopyOption.REPLACE_EXISTING)) { throw new FileAlreadyExistsException(target.toString()); } } if (sourceResult.getK1() == NOT_FOUND) { throw new NoSuchFileException(target.toString()); } if (!source.getRefTree().equals(target.getRefTree())) { copyAssetContent(source, target, options); } else if (!source.getFileSystem().equals(target.getFileSystem())) { copyAssetContent(source, target, options); } else { final Map<JGitPathImpl, JGitPathImpl> sourceDest = new HashMap<JGitPathImpl, JGitPathImpl>(); if (sourceResult.getK1() == DIRECTORY) { sourceDest.putAll(mapDirectoryContent(source, target, options)); } else { sourceDest.put(source, target); } copyFiles(source, target, sourceDest, options); } } private void copyAssetContent(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final Pair<PathType, ObjectId> sourceResult = checkPath(source.getFileSystem().gitRepo(), source.getRefTree(), source.getPath()); final Pair<PathType, ObjectId> targetResult = checkPath(target.getFileSystem().gitRepo(), target.getRefTree(), target.getPath()); if (!isRoot(target) && targetResult.getK1() != NOT_FOUND) { if (!contains(options, StandardCopyOption.REPLACE_EXISTING)) { throw new FileAlreadyExistsException(target.toString()); } } if (sourceResult.getK1() == NOT_FOUND) { throw new NoSuchFileException(target.toString()); } if (sourceResult.getK1() == DIRECTORY) { copyDirectory(source, target, options); return; } copyFile(source, target, options); } private boolean contains(final CopyOption[] options, final CopyOption opt) { for (final CopyOption option : options) { if (option.equals(opt)) { return true; } } return false; } private void copyDirectory(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final List<JGitPathImpl> directories = new ArrayList<JGitPathImpl>(); for (final Path path : newDirectoryStream(source, null)) { final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> pathResult = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (pathResult.getK1() == DIRECTORY) { directories.add(gPath); continue; } final JGitPathImpl gTarget = composePath(target, (JGitPathImpl) gPath.getFileName()); copyFile(gPath, gTarget); } for (final JGitPathImpl directory : directories) { createDirectory(composePath(target, (JGitPathImpl) directory.getFileName())); } } private JGitPathImpl composePath(final JGitPathImpl directory, final JGitPathImpl fileName, final CopyOption... options) { if (directory.getPath().endsWith("/")) { return toPathImpl(getPath(URI.create(directory.toUri().toString() + uriEncode(fileName.toString(false))))); } return toPathImpl(getPath(URI.create(directory.toUri().toString() + "/" + uriEncode(fileName.toString(false))))); } private String uriEncode(final String s) { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { return s; } } private void copyFile(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final InputStream in = newInputStream(source, convert(options)); final SeekableByteChannel out = newByteChannel(target, new HashSet<OpenOption>() {{ add(StandardOpenOption.TRUNCATE_EXISTING); for (final CopyOption _option : options) { if (_option instanceof OpenOption) { add((OpenOption) _option); } } }}); try { int count; byte[] buffer = new byte[8192]; while ((count = in.read(buffer)) > 0) { out.write(ByteBuffer.wrap(buffer, 0, count)); } } catch (Exception e) { throw new IOException("Failed to copy file from '" + source + "' to '" + target + "'", e); } finally { try { out.close(); } catch (java.io.IOException e) { throw new IOException("Could not close output stream.", e); } finally { try { in.close(); } catch (java.io.IOException e) { throw new IOException("Could not close input stream.", e); } } } } private OpenOption[] convert(CopyOption... options) { if (options == null || options.length == 0) { return new OpenOption[0]; } final List<OpenOption> newOptions = new ArrayList<OpenOption>(options.length); for (final CopyOption option : options) { if (option instanceof OpenOption) { newOptions.add((OpenOption) option); } } return newOptions.toArray(new OpenOption[newOptions.size()]); } private void createBranch(final JGitPathImpl source, final JGitPathImpl target) { try { target.getFileSystem().lock(); JGitUtil.createBranch(source.getFileSystem().gitRepo(), source.getRefTree(), target.getRefTree()); } finally { target.getFileSystem().unlock(); } } private boolean existsBranch(final JGitPathImpl path) { return hasBranch(path.getFileSystem().gitRepo(), path.getRefTree()); } private boolean isBranch(final JGitPathImpl path) { return path.getPath().length() == 1 && path.getPath().equals("/"); } private boolean isRoot(final JGitPathImpl path) { return isBranch(path); } private boolean hasSameFileSystem(final JGitPathImpl source, final JGitPathImpl target) { return source.getFileSystem().equals(target); } @Override public void move(final Path source, final Path target, final CopyOption... options) throws DirectoryNotEmptyException, AtomicMoveNotSupportedException, IOException, SecurityException { checkNotNull("source", source); checkNotNull("target", target); final JGitPathImpl gSource = toPathImpl(source); final JGitPathImpl gTarget = toPathImpl(target); final boolean isSourceBranch = isBranch(gSource); final boolean isTargetBranch = isBranch(gTarget); if (isSourceBranch && isTargetBranch) { moveBranch(gSource, gTarget, options); return; } moveAsset(gSource, gTarget, options); } private void moveBranch(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { checkCondition("source and taget should have same setup", !hasSameFileSystem(source, target)); if (!exists(source)) { throw new NoSuchFileException(target.toString()); } boolean targetExists = existsBranch(target); if (targetExists && !contains(options, StandardCopyOption.REPLACE_EXISTING)) { throw new FileAlreadyExistsException(target.toString()); } if (!targetExists) { createBranch(source, target); deleteBranch(source); } else { commit(target, buildCommitInfo("reverting from {" + source.getPath() + "}", Arrays.asList(options)), new RevertCommitContent(source.getRefTree())); } } private void moveAsset(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final Pair<PathType, ObjectId> sourceResult = checkPath(source.getFileSystem().gitRepo(), source.getRefTree(), source.getPath()); final Pair<PathType, ObjectId> targetResult = checkPath(target.getFileSystem().gitRepo(), target.getRefTree(), target.getPath()); if (!isRoot(target) && targetResult.getK1() != NOT_FOUND) { if (!contains(options, StandardCopyOption.REPLACE_EXISTING)) { throw new FileAlreadyExistsException(target.toString()); } } if (sourceResult.getK1() == NOT_FOUND) { throw new NoSuchFileException(target.toString()); } if (!source.getRefTree().equals(target.getRefTree())) { copy(source, target, options); delete(source); } else { final Map<JGitPathImpl, JGitPathImpl> fromTo = new HashMap<JGitPathImpl, JGitPathImpl>(); if (sourceResult.getK1() == DIRECTORY) { fromTo.putAll(mapDirectoryContent(source, target, options)); } else { fromTo.put(source, target); } moveFiles(source, target, fromTo, options); } } private Map<JGitPathImpl, JGitPathImpl> mapDirectoryContent(final JGitPathImpl source, final JGitPathImpl target, final CopyOption... options) { final Map<JGitPathImpl, JGitPathImpl> fromTo = new HashMap<JGitPathImpl, JGitPathImpl>(); for (final Path path : newDirectoryStream(source, null)) { final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> pathResult = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (pathResult.getK1() == DIRECTORY) { fromTo.putAll(mapDirectoryContent(gPath, composePath(target, (JGitPathImpl) gPath.getFileName()))); } else { final JGitPathImpl gTarget = composePath(target, (JGitPathImpl) gPath.getFileName()); fromTo.put(gPath, gTarget); } } return fromTo; } private void moveFiles(final JGitPathImpl source, final JGitPathImpl target, final Map<JGitPathImpl, JGitPathImpl> fromTo, final CopyOption... options) { final Map<String, String> result = new HashMap<String, String>(fromTo.size()); for (final Map.Entry<JGitPathImpl, JGitPathImpl> fromToEntry : fromTo.entrySet()) { result.put(fixPath(fromToEntry.getKey().getPath()), fixPath(fromToEntry.getValue().getPath())); } commit(source, buildCommitInfo("moving from {" + source.getPath() + "} to {" + target.getPath() + "}", Arrays.asList(options)), new MoveCommitContent(result)); } private void copyFiles(final JGitPathImpl source, final JGitPathImpl target, final Map<JGitPathImpl, JGitPathImpl> sourceDest, final CopyOption... options) { final Map<String, String> result = new HashMap<String, String>(sourceDest.size()); for (final Map.Entry<JGitPathImpl, JGitPathImpl> sourceDestEntry : sourceDest.entrySet()) { result.put(fixPath(sourceDestEntry.getKey().getPath()), fixPath(sourceDestEntry.getValue().getPath())); } commit(source, buildCommitInfo("copy from {" + source.getPath() + "} to {" + target.getPath() + "}", Arrays.asList(options)), new CopyCommitContent(result)); } @Override public boolean isSameFile(final Path pathA, final Path pathB) throws IOException, SecurityException { checkNotNull("pathA", pathA); checkNotNull("pathB", pathB); final JGitPathImpl gPathA = toPathImpl(pathA); final JGitPathImpl gPathB = toPathImpl(pathB); final Pair<PathType, ObjectId> resultA = checkPath(gPathA.getFileSystem().gitRepo(), gPathA.getRefTree(), gPathA.getPath()); final Pair<PathType, ObjectId> resultB = checkPath(gPathB.getFileSystem().gitRepo(), gPathB.getRefTree(), gPathB.getPath()); if (resultA.getK1() == PathType.FILE && resultA.getK2().equals(resultB.getK2())) { return true; } return pathA.equals(pathB); } @Override public boolean isHidden(final Path path) throws IllegalArgumentException, IOException, SecurityException { checkNotNull("path", path); final JGitPathImpl gPath = toPathImpl(path); if (gPath.getFileName() == null) { return false; } return toPathImpl(path.getFileName()).toString(false).startsWith("."); } @Override public FileStore getFileStore(final Path path) throws IOException, SecurityException { checkNotNull("path", path); return new JGitFileStore(toPathImpl(path).getFileSystem().gitRepo().getRepository()); } @Override public void checkAccess(final Path path, final AccessMode... modes) throws UnsupportedOperationException, NoSuchFileException, AccessDeniedException, IOException, SecurityException { checkNotNull("path", path); final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> result = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (result.getK1().equals(NOT_FOUND)) { throw new NoSuchFileException(path.toString()); } } @Override public <V extends FileAttributeView> V getFileAttributeView(final Path path, final Class<V> type, final LinkOption... options) throws NoSuchFileException { checkNotNull("path", path); checkNotNull("type", type); final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> pathResult = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (pathResult.getK1().equals(NOT_FOUND)) { throw new NoSuchFileException(path.toString()); } final V resultView = gPath.getAttrView(type); if (resultView == null) { if (type == BasicFileAttributeView.class || type == JGitBasicAttributeView.class) { final V newView = (V) new JGitBasicAttributeView(gPath); gPath.addAttrView(newView); return newView; } else if (type == HiddenAttributeView.class || type == JGitHiddenAttributeView.class) { final V newView = (V) new JGitHiddenAttributeView(gPath); gPath.addAttrView(newView); return newView; } else if (type == VersionAttributeView.class || type == JGitVersionAttributeView.class) { final V newView = (V) new JGitVersionAttributeView(gPath); gPath.addAttrView(newView); return newView; } } return resultView; } private ExtendedAttributeView getFileAttributeView(final JGitPathImpl path, final String name, final LinkOption... options) { final ExtendedAttributeView view = path.getAttrView(name); if (view == null) { if (name.equals("basic")) { final JGitBasicAttributeView newView = new JGitBasicAttributeView(path); path.addAttrView(newView); return newView; } else if (name.equals("extended")) { final JGitHiddenAttributeView newView = new JGitHiddenAttributeView(path); path.addAttrView(newView); return newView; } else if (name.equals("version")) { final JGitVersionAttributeView newView = new JGitVersionAttributeView(path); path.addAttrView(newView); return newView; } } return view; } @Override public <A extends BasicFileAttributes> A readAttributes(final Path path, final Class<A> type, final LinkOption... options) throws NoSuchFileException, UnsupportedOperationException, IOException, SecurityException { checkNotNull("path", path); checkNotNull("type", type); final JGitPathImpl gPath = toPathImpl(path); final Pair<PathType, ObjectId> pathResult = checkPath(gPath.getFileSystem().gitRepo(), gPath.getRefTree(), gPath.getPath()); if (pathResult.getK1().equals(NOT_FOUND)) { throw new NoSuchFileException(path.toString()); } if (type == VersionAttributes.class) { final JGitVersionAttributeView view = getFileAttributeView(path, JGitVersionAttributeView.class, options); return (A) view.readAttributes(); } else if (type == HiddenAttributes.class) { final JGitHiddenAttributeView view = getFileAttributeView(path, JGitHiddenAttributeView.class, options); return (A) view.readAttributes(); } else if (type == BasicFileAttributesImpl.class || type == BasicFileAttributes.class) { final JGitBasicAttributeView view = getFileAttributeView(path, JGitBasicAttributeView.class, options); return (A) view.readAttributes(); } return null; } @Override public Map<String, Object> readAttributes(final Path path, final String attributes, final LinkOption... options) throws UnsupportedOperationException, IllegalArgumentException, IOException, SecurityException { checkNotNull("path", path); checkNotEmpty("attributes", attributes); final String[] s = split(attributes); if (s[0].length() == 0) { throw new IllegalArgumentException(attributes); } if (s[0].equals("diff")) { final Repository repo = ((JGitPathImpl) path).getFileSystem().gitRepo().getRepository(); final String[] branches = s[1].split(","); final String branchA = branches[0]; final String branchB = branches[1]; final List<FileDiff> diffs = new DiffBranches(repo, branchA, branchB).execute().get(); final HashMap<String, Object> map = new HashMap<>(); map.put("diff", diffs); return map; } else { final ExtendedAttributeView view = getFileAttributeView(toPathImpl(path), s[0], options); if (view == null) { throw new UnsupportedOperationException("View '" + s[0] + "' not available"); } return view.readAttributes(s[1].split(",")); } } @Override public void setAttribute(final Path path, final String attribute, final Object value, final LinkOption... options) throws UnsupportedOperationException, IllegalArgumentException, ClassCastException, IOException, SecurityException { checkNotNull("path", path); checkNotEmpty("attributes", attribute); if (attribute.equals(SquashOption.SQUASH_ATTR) && value instanceof SquashOption) { this.lockAndSquash(path, (SquashOption) value); return; } if (attribute.equals(FileSystemState.FILE_SYSTEM_STATE_ATTR)) { JGitFileSystem fileSystem = (JGitFileSystem) path.getFileSystem(); try { fileSystem.lock(); if (value instanceof CommentedOption) { fileSystem.setBatchCommitInfo("Batch mode", (CommentedOption) value); fileSystem.unlock(); return; } final boolean isOriginalStateBatch = fileSystem.isOnBatch(); fileSystem.setState(value.toString()); FileSystemState.valueOf(value.toString()); if (isOriginalStateBatch && !fileSystem.isOnBatch()) { fileSystem.setBatchCommitInfo(null); notifyAllDiffs(); } fileSystem.setHadCommitOnBatchState(false); } finally { fileSystem.unlock(); } return; } final String[] s = split(attribute); if (s[0].length() == 0) { throw new IllegalArgumentException(attribute); } final ExtendedAttributeView view = getFileAttributeView(toPathImpl(path), s[0], options); if (view == null) { throw new UnsupportedOperationException("View '" + s[0] + "' not available"); } view.setAttribute(s[1], value); } private void lockAndSquash(final Path path, final SquashOption value) { JGitFileSystem fileSystem = (JGitFileSystem) path.getFileSystem(); try { fileSystem.lock(); final JGitPathImpl gSource = toPathImpl(path); final Git git = gSource.getFileSystem().gitRepo(); String branch = getBranchName(gSource); String commitMessage = checkNotEmpty("commitMessage", value.getMessage()); String startCommit = checkNotEmpty("startCommit", value.getRecord().id()); new Squash(git, branch, startCommit, commitMessage).execute(); } finally { fileSystem.unlock(); } } private String getBranchName(final JGitPathImpl gSource) { try { return gSource.getFileSystem().gitRepo().getRepository().getBranch(); } catch (java.io.IOException e) { throw new IOException("Impossible to get Branch Name", e); } } private void checkURI(final String paramName, final URI uri) throws IllegalArgumentException { checkNotNull("uri", uri); if (uri.getAuthority() == null || uri.getAuthority().isEmpty()) { throw new IllegalArgumentException("Parameter named '" + paramName + "' is invalid, missing host repository!"); } int atIndex = uri.getPath().indexOf("@"); if (atIndex != -1 && !uri.getAuthority().contains("@")) { if (uri.getPath().indexOf("/", atIndex) == -1) { throw new IllegalArgumentException("Parameter named '" + paramName + "' is invalid, missing host repository!"); } } } public String extractHost(final URI uri) { checkNotNull("uri", uri); String host = uri.getAuthority(); String path = uri.getPath(); int atIndex = path.indexOf("@"); if (atIndex != -1 && !uri.getAuthority().contains("@")) { int slashAfterAt = path.indexOf("/", path.indexOf("/", atIndex) + 1); if (slashAfterAt != -1) { return host + path.substring(0, slashAfterAt); } else { return host; } } else { int secondSlash = path.indexOf("/", 1); if (secondSlash != -1) { return host + path.substring(0, path.indexOf("/", 1)); } else { return host + path; } } } public String extractOldHost(final URI uri) { checkNotNull("uri", uri); int atIndex = uri.getPath().indexOf("@"); if (atIndex != -1 && !uri.getAuthority().contains("@")) { return uri.getAuthority() + uri.getPath().substring(0, uri.getPath().indexOf("/", atIndex)); } return uri.getAuthority(); } public String extractRepoNameWithFolder(final URI uri) { checkNotNull("uri", uri); String host = extractHost(uri); int index = host.indexOf('@'); if (index != -1) { host = host.substring(index + 1); } String path = uri.getPath(); String complex = "@" + host; int pathIndex = path.indexOf(complex); if (pathIndex != -1) { path = path.substring(pathIndex + complex.length()); } return host; } private String extractRepoNameWithoutFolder(final URI uri) { checkNotNull("uri", uri); final String host = extractOldHost(uri); int index = host.indexOf('@'); if (index != -1) { return host.substring(index + 1); } return host; } private boolean hasSyncFlag(final URI uri) { checkNotNull("uri", uri); return uri.getQuery() != null && uri.getQuery().contains("sync"); } private boolean hasForceFlag(URI uri) { checkNotNull("uri", uri); return uri.getQuery() != null && uri.getQuery().contains("force"); } private boolean hasPushFlag(final URI uri) { checkNotNull("uri", uri); return uri.getQuery() != null && uri.getQuery().contains("push"); } public String extractPath(final URI uri) { checkNotNull("uri", uri); final String repoName = extractRepoNameWithFolder(uri); int authority = 0; int index = uri.getAuthority().indexOf("@"); if (index > 0) { authority = uri.getAuthority().substring(0, index + 1).length(); } int secondIndex = uri.getPath().indexOf("@"); if (secondIndex != -1) { authority = uri.getAuthority().length(); authority = authority + uri.getPath().substring(0, secondIndex + 1).length(); } final String path = EncodingUtil.decode(uri.toString()).substring(getSchemeSize(uri) + authority + repoName.length()); if (path.startsWith("/:")) { return path.substring(2); } return path; } public String extractOldPath(final URI uri) { checkNotNull("uri", uri); final String host = extractOldHost(uri); final String path = EncodingUtil.decode(uri.toString()).substring(getSchemeSize(uri) + host.length()); if (path.startsWith("/:")) { return path.substring(2); } return path; } private CredentialsProvider buildCredential(final Map<String, ?> env) { if (env != null) { if (env.containsKey(GIT_ENV_KEY_USER_NAME)) { if (env.containsKey(GIT_ENV_KEY_PASSWORD)) { return new UsernamePasswordCredentialsProvider(env.get(GIT_ENV_KEY_USER_NAME).toString(), env.get(GIT_ENV_KEY_PASSWORD).toString()); } return new UsernamePasswordCredentialsProvider(env.get(GIT_ENV_KEY_USER_NAME).toString(), ""); } } return CredentialsProvider.getDefault(); } private JGitPathImpl toPathImpl(final Path path) { if (path instanceof JGitPathImpl) { return (JGitPathImpl) path; } throw new IllegalArgumentException("Path not supported by current provider."); } private String[] split(final String attribute) { final String[] s = new String[2]; final int pos = attribute.indexOf(':'); if (pos == -1) { s[0] = "basic"; s[1] = attribute; } else { s[0] = attribute.substring(0, pos); s[1] = (pos == attribute.length()) ? "" : attribute.substring(pos + 1); } return s; } private int getSchemeSize(final URI uri) { if (uri.getScheme().equals(SCHEME)) { return SCHEME_SIZE; } return DEFAULT_SCHEME_SIZE; } private void delete(final JGitPathImpl path, final CommitInfo commitInfo) { commit(path, commitInfo, new DefaultCommitContent(new HashMap<String, File>() {{ put(path.getPath(), null); }})); } private void commit(final JGitPathImpl path, final CommitInfo commitInfo, final CommitContent commitContent) { final JGitFileSystem fileSystem = path.getFileSystem(); try { fileSystem.lock(); final Git git = fileSystem.gitRepo(); final String branchName = path.getRefTree(); final boolean batchState = fileSystem.isOnBatch(); final boolean amend = batchState && fileSystem.isHadCommitOnBatchState(path.getRoot()); final ObjectId oldHead = JGitUtil.getTreeRefObjectId(path.getFileSystem().gitRepo().getRepository(), branchName); final boolean hasCommit; if (batchState && fileSystem.getBatchCommitInfo() != null) { hasCommit = JGitUtil.commit(git, branchName, fileSystem.getBatchCommitInfo(), amend, commitContent); } else { hasCommit = JGitUtil.commit(git, branchName, commitInfo, amend, commitContent); } if (!batchState) { if (hasCommit) { int value = fileSystem.incrementAndGetCommitCount(); if (value >= commitLimit) { JGitUtil.gc(git); fileSystem.resetCommitCount(); } } final ObjectId newHead = JGitUtil.getTreeRefObjectId(path.getFileSystem().gitRepo().getRepository(), branchName); postCommitHook(git.getRepository()); notifyDiffs(path.getFileSystem(), branchName, commitInfo.getSessionId(), commitInfo.getName(), commitInfo.getMessage(), oldHead, newHead); } else { synchronized (oldHeadsOfPendingDiffsLock) { if (!oldHeadsOfPendingDiffs.containsKey(path.getFileSystem()) || !oldHeadsOfPendingDiffs.get(path.getFileSystem()).containsKey(branchName)) { if (!oldHeadsOfPendingDiffs.containsKey(path.getFileSystem())) { oldHeadsOfPendingDiffs.put(path.getFileSystem(), new ConcurrentHashMap<String, NotificationModel>()); } if (fileSystem.getBatchCommitInfo() != null) { oldHeadsOfPendingDiffs.get(path.getFileSystem()).put(branchName, new NotificationModel(oldHead, fileSystem.getBatchCommitInfo().getSessionId(), fileSystem.getBatchCommitInfo().getName(), fileSystem.getBatchCommitInfo().getMessage())); } else { oldHeadsOfPendingDiffs.get(path.getFileSystem()).put(branchName, new NotificationModel(oldHead, commitInfo.getSessionId(), commitInfo.getName(), commitInfo.getMessage())); } } } } if (path.getFileSystem().isOnBatch() && !fileSystem.isHadCommitOnBatchState(path.getRoot())) { fileSystem.setHadCommitOnBatchState(path.getRoot(), hasCommit); } } finally { fileSystem.unlock(); } } private void postCommitHook(final Repository repository) { detectedFS.runHookIfPresent(repository, "post-commit", new String[0]); } private void notifyAllDiffs() { synchronized (oldHeadsOfPendingDiffsLock) { for (Map.Entry<JGitFileSystem, Map<String, NotificationModel>> jGitFileSystemMapEntry : oldHeadsOfPendingDiffs.entrySet()) { for (Map.Entry<String, NotificationModel> branchNameNotificationModelEntry : jGitFileSystemMapEntry.getValue().entrySet()) { final ObjectId newHead = JGitUtil.getTreeRefObjectId(jGitFileSystemMapEntry.getKey().gitRepo().getRepository(), branchNameNotificationModelEntry.getKey()); try { notifyDiffs(jGitFileSystemMapEntry.getKey(), branchNameNotificationModelEntry.getKey(), branchNameNotificationModelEntry.getValue().getSessionId(), branchNameNotificationModelEntry.getValue().getUserName(), branchNameNotificationModelEntry.getValue().getMessage(), branchNameNotificationModelEntry.getValue().getOriginalHead(), newHead); } catch (final Exception ex) { LOG.error(String.format("Couldn't produce diff notification for repository `%s` branch `%s`.", jGitFileSystemMapEntry.getKey().toString(), branchNameNotificationModelEntry.getKey()), ex); } } } for (JGitFileSystem fileSystem : oldHeadsOfPendingDiffs.keySet()) { int value = fileSystem.incrementAndGetCommitCount(); if (value >= commitLimit) { JGitUtil.gc(fileSystem.gitRepo()); fileSystem.resetCommitCount(); } } oldHeadsOfPendingDiffs.clear(); } } void notifyDiffs(final JGitFileSystem fs, final String _tree, final String sessionId, final String userName, final String message, final ObjectId oldHead, final ObjectId newHead) { final String tree; if (_tree.startsWith("refs/")) { tree = _tree.substring(_tree.lastIndexOf("/") + 1); } else { tree = _tree; } final String host = tree + "@" + fs.getName(); final Path root = JGitPathImpl.createRoot(fs, "/", host, false); final List<DiffEntry> diff = JGitUtil.getDiff(fs.gitRepo().getRepository(), oldHead, newHead); final List<WatchEvent<?>> events = new ArrayList<WatchEvent<?>>(diff.size()); for (final DiffEntry diffEntry : diff) { final Path oldPath; if (!diffEntry.getOldPath().equals(DiffEntry.DEV_NULL)) { oldPath = JGitPathImpl.create(fs, "/" + diffEntry.getOldPath(), host, null, false); } else { oldPath = null; } final Path newPath; if (!diffEntry.getNewPath().equals(DiffEntry.DEV_NULL)) { JGitPathInfo pathInfo = resolvePath(fs.gitRepo(), tree, diffEntry.getNewPath()); newPath = JGitPathImpl.create(fs, "/" + pathInfo.getPath(), host, pathInfo.getObjectId(), false); } else { newPath = null; } events.add(new WatchEvent() { @Override public Kind kind() { DiffEntry.ChangeType changeType = diffEntry.getChangeType(); switch (changeType) { case ADD: case COPY: return StandardWatchEventKind.ENTRY_CREATE; case DELETE: return StandardWatchEventKind.ENTRY_DELETE; case MODIFY: return StandardWatchEventKind.ENTRY_MODIFY; case RENAME: return StandardWatchEventKind.ENTRY_RENAME; default: throw new RuntimeException("Unsupported change type: " + changeType); } } @Override public int count() { return 1; } @Override public Object context() { return new WatchContext() { @Override public Path getPath() { return newPath; } @Override public Path getOldPath() { return oldPath; } @Override public String getSessionId() { return sessionId; } @Override public String getMessage() { return message; } @Override public String getUser() { return userName; } }; } @Override public String toString() { return "WatchEvent{" + "newPath=" + newPath + ", oldPath=" + oldPath + ", sessionId='" + sessionId + '\'' + ", userName='" + userName + '\'' + ", message='" + message + '\'' + ", changeType=" + diffEntry.getChangeType() + '}'; } }); } if (!events.isEmpty()) { fs.publishEvents(root, events); } } GitSSHService getGitSSHService() { return gitSSHService; } public void setDetectedFS(final FS detectedFS) { this.detectedFS = detectedFS; } /** * Adapts a {@link SimpleAsyncExecutorService} to an {@link Executor} because SimpleAsyncExecutorService can't * implement Executor directly due to bugs in some older CDI implementations. */ private static class ExecutorWrapper implements Executor { private final DisposableExecutor simpleAsyncExecutor; public ExecutorWrapper(DisposableExecutor simpleAsyncExecutor) { this.simpleAsyncExecutor = checkNotNull("simpleAsyncExecutor", simpleAsyncExecutor); } @Override public void execute(Runnable command) { simpleAsyncExecutor.execute(command); } } public class RepositoryResolverImpl<T> implements RepositoryResolver<T> { @Override public Repository open(final T client, final String name) throws RepositoryNotFoundException, ServiceNotAuthorizedException, ServiceNotEnabledException, ServiceMayNotContinueException { final JGitFileSystem fs = fileSystems.get(name); if (fs == null) { throw new RepositoryNotFoundException(name); } return fs.gitRepo().getRepository(); } public JGitFileSystem resolveFileSystem(final Repository repository) { return repoIndex.get(repository); } } }