Java tutorial
/******************************************************************************* * Copyright (c) 2010, 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.orion.server.filesystem.git; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.net.URLStreamHandler; import java.util.Collection; import org.apache.commons.io.FileUtils; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileInfo; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.filesystem.provider.FileInfo; import org.eclipse.core.filesystem.provider.FileStore; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.egit.core.op.CloneOperation; import org.eclipse.jgit.api.AddCommand; import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.PullCommand; import org.eclipse.jgit.api.PullResult; import org.eclipse.jgit.api.PushCommand; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectIdRef.PeeledNonTag; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.storage.file.FileRepository; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FS; import org.eclipse.orion.internal.server.filesystem.git.Activator; import org.eclipse.orion.internal.server.filesystem.git.OrionUserCredentialsProvider; import org.eclipse.orion.internal.server.filesystem.git.Utils; import org.eclipse.orion.server.core.LogHelper; public class GitFileStore extends FileStore { private Repository localRepo; private URL gitUrl; private String authority; public GitFileStore(String s, String authority) { try { gitUrl = new URL(null, s, new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { // never called, see #openInputStream(int, IProgressMonitor) return null; } }); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } if (authority == null) throw new IllegalArgumentException("authority cannot be null"); this.authority = authority; if (gitUrl.getQuery() == null) throw new IllegalArgumentException("missing query"); } public URL getUrl() { return gitUrl; } // TODO: to field private File getWorkingDir() { IPath location = Activator.getDefault().getPlatformLocation(); // <workspace path> if (location == null) throw new RuntimeException("Unable to compute local file system location"); //$NON-NLS-1$; // TODO: just for now location = location.append("PRIVATE_REPO").append(authority).append(gitUrl.getProtocol()) .append(gitUrl.getHost()).append(gitUrl.getPath()); // <workspace path>/PRIVATE_REPO/<username>/<protocol>/<host>/<path>/<repo> return location.toFile(); } // TODO: to field public File getLocalFile() { String q = gitUrl.getQuery(); String p = getWorkingDir().getAbsolutePath() + "/" + q; return new File(p); } public Repository getLocalRepo() throws IOException { if (localRepo == null) { IPath p = new Path(getWorkingDir().getAbsolutePath()).append(Constants.DOT_GIT); // <absolute path to working dir>/.git localRepo = new FileRepository(p.toFile()); } return localRepo; } public IFileInfo[] childInfos(int options, IProgressMonitor monitor) throws CoreException { IFileStore[] childStores = childStores(options, monitor); IFileInfo[] childInfos = new IFileInfo[childStores.length]; for (int i = 0; i < childStores.length; i++) { childInfos[i] = childStores[i].fetchInfo(EFS.CACHE /* don't pull */, monitor); } return childInfos; } @Override public String[] childNames(int options, IProgressMonitor monitor) throws CoreException { if (!isCloned()) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "A private clone for " + this + " doesn't exist." + getLocalFile(), null)); } pull(); File f = getLocalFile(); return f.list(null); } public boolean isCloned() { return getWorkingDir().exists() && RepositoryCache.FileKey .isGitRepository(new File(getWorkingDir(), Constants.DOT_GIT), FS.DETECTED); } @Override public IFileInfo fetchInfo(int options, IProgressMonitor monitor) throws CoreException { if (!isCloned()) { initCloneCommitPush(monitor); } if ((options & EFS.CACHE) == 0) // don't use cache pull(); FileInfo fi = new FileInfo(); fi.setName(getName()); File f = getLocalFile(); fi.setExists(f.exists()); fi.setDirectory(f.isDirectory()); // TODO: remote commit time? // fi.setLastModified(0); return fi; } @Override public IFileStore getChild(String name) { String s = gitUrl.toString(); if (s.endsWith("/")) s = s + name; else s = s + "/" + name; return new GitFileStore(s, authority); } @Override public String getName() { String q = gitUrl.getQuery(); Path p = new Path(q); return p.lastSegment() != null ? p.lastSegment() : "" /* root */; } @Override public IFileStore getParent() { String q = gitUrl.getQuery(); Path p = new Path(q); // return null for the root store if (p.isRoot()) return null; IPath ip = p.removeLastSegments(1); String s = gitUrl.toString(); s = s.substring(0, s.indexOf(q)); return new GitFileStore(s + ip.toString(), authority); } @Override public InputStream openInputStream(int options, IProgressMonitor monitor) throws CoreException { pull(); try { return new FileInputStream(getLocalFile()); } catch (FileNotFoundException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "I/O exception while opening input stream for: " + getLocalFile(), null)); } } @Override public URI toURI() { StringBuffer sb = new StringBuffer(); sb.append(GitFileSystem.SCHEME_GIT); sb.append(":/"); // TODO: include authority? try { sb.append(URLEncoder.encode(gitUrl.toString(), "UTF-8")); } catch (UnsupportedEncodingException e) { sb.append(URLEncoder.encode(gitUrl.toString())); } // return // org.eclipse.core.runtime.URIUtil.fromString(String)(sb.toString()); return URI.create(sb.toString()); } @Override public IFileStore mkdir(int options, IProgressMonitor monitor) throws CoreException { boolean deep = (options & EFS.SHALLOW) == 0; if (isRoot()) { initCloneCommitPush(monitor); } else { File f = getLocalFile(); if (f.getParentFile().exists() && !f.getParentFile().isDirectory()) { throw new CoreException( new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Local parent is a file: " + f, null)); } if (deep) { GitFileStore root = (GitFileStore) Utils.getRoot(this); root.initCloneCommitPush(monitor); f.mkdirs(); } else { // TODO: sync with remote first if (f.getParentFile().exists()) { f.mkdir(); } else { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Local parent does not exist: " + f, null)); } } commit(true); push(); } return this; } public OutputStream openOutputStream(final int options, IProgressMonitor monitor) throws CoreException { File f = getLocalFile(); if (!f.getParentFile().exists()) { throw new CoreException( new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Local parent does not exist: " + f, null)); } return new ByteArrayOutputStream() { public void close() throws IOException { super.close(); setContents(toByteArray(), (options & EFS.APPEND) != 0); } }; } void setContents(byte[] bytes, boolean append) throws IOException { File f = getLocalFile(); try { byte[] contents = bytes; if (append) { byte[] oldContents; if (f.exists()) { FileInputStream fis = new FileInputStream(f); oldContents = readBytes(fis); fis.close(); } else { oldContents = new byte[0]; } byte[] newContents = new byte[oldContents.length + bytes.length]; System.arraycopy(oldContents, 0, newContents, 0, oldContents.length); System.arraycopy(bytes, 0, newContents, oldContents.length, bytes.length); contents = newContents; } FileOutputStream fos = new FileOutputStream(f); fos.write(contents); fos.close(); commit(false); pull(); push(); } catch (CoreException e) { throw new IOException(e.getMessage()); } } public boolean isRoot() { return getUrl().getQuery().equals("/"); } @Override public void delete(int options, IProgressMonitor monitor) throws CoreException { if (isRoot()) { try { getLocalRepo().close(); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Unable to close cloned repository before deleting : " + this, e)); } } File f = getLocalFile(); try { FileUtils.forceDelete(f); rm(); } catch (FileNotFoundException e) { // ignore } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Unable to delete a file when deleting : " + this, e)); } } private static byte[] readBytes(InputStream is) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int r; byte[] data = new byte[16384]; while ((r = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, r); } buffer.flush(); return buffer.toByteArray(); } /** * Clones from shared repo over local transport, creates the repo when * necessary and inits it by pushing a dummy change (.gitignore) file to * remote */ private void initCloneCommitPush(IProgressMonitor monitor) throws CoreException { boolean inited = false; // if it's a local repository try to init it first if (canInit()) { try { inited = initBare(); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Could not init local bare repo: " + this, e)); } } clone(monitor); if (inited) { commit(true); push(); } } private boolean canInit() { try { // org.eclipse.jgit.transport.TransportLocal.canHandle(URIish, FS) URIish uri = Utils.toURIish(getUrl()); if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null || uri.getPass() != null || uri.getPath() == null) return false; if ("file".equals(uri.getScheme()) || uri.getScheme() == null) return true; } catch (URISyntaxException e) { LogHelper.log(new Status(IStatus.ERROR, Activator.PI_GIT, 1, "Cannot init" + this + ". The URL cannot be parsed as a URI reference", e)); } return false; } private CredentialsProvider getCredentialsProvider() { try { return new OrionUserCredentialsProvider(authority, Utils.toURIish(getUrl())); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } private void clone(IProgressMonitor monitor) throws CoreException { try { URIish uri = Utils.toURIish(getUrl()); File workdir = getWorkingDir(); if (!isCloned()) { workdir.mkdirs(); // TODO: ListRemoteOperation.getRemoteRef Ref ref = new PeeledNonTag(Ref.Storage.NETWORK, "refs/heads/master", null); final CloneOperation op = new CloneOperation(uri, true, null, workdir, ref, "origin", 0); op.setCredentialsProvider(getCredentialsProvider()); op.run(monitor); LogHelper.log( new Status(IStatus.INFO, Activator.PI_GIT, 1, "Cloned " + this + " to " + workdir, null)); } } catch (InterruptedException e) { // ignore } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, e.getMessage(), e)); } } private boolean initBare() throws URISyntaxException, IOException { String scheme = getUrl().getProtocol(); String path = getUrl().getPath(); if (scheme != null && !scheme.equals("file")) { throw new IllegalArgumentException("#canInit() has mistaken, this is not a local file system URL"); } File sharedRepo = new File(path); // remember, we know how to init only local repositories if (sharedRepo.exists() && RepositoryCache.FileKey.isGitRepository(new File(sharedRepo, Constants.DOT_GIT), FS.DETECTED)) { // nothing to init, a repository already exists at the given location return false; } sharedRepo.mkdir(); LogHelper.log( new Status(IStatus.INFO, Activator.PI_GIT, 1, "Initializing bare repository for " + this, null)); FileRepository repository = new FileRepository(new File(sharedRepo, Constants.DOT_GIT)); repository.create(true); return true; } private void push() throws CoreException { try { Repository local = getLocalRepo(); Git git = new Git(local); PushCommand push = git.push(); push.setRefSpecs(new RefSpec("refs/heads/*:refs/heads/*")); push.setCredentialsProvider(getCredentialsProvider()); Iterable<PushResult> pushResults = push.call(); for (PushResult pushResult : pushResults) { Collection<RemoteRefUpdate> updates = pushResult.getRemoteUpdates(); for (RemoteRefUpdate update : updates) { org.eclipse.jgit.transport.RemoteRefUpdate.Status status = update.getStatus(); if (status.equals(org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK) || status.equals(org.eclipse.jgit.transport.RemoteRefUpdate.Status.UP_TO_DATE)) { LogHelper.log(new Status(IStatus.INFO, Activator.PI_GIT, 1, "Push succeed: " + this, null)); } else { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, status.toString(), null)); } } } } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, e.getMessage(), e)); } } /** * pulls from the remote * @throws CoreException */ private void pull() throws CoreException { Transport transport = null; try { Repository repo = getLocalRepo(); Git git = new Git(repo); PullCommand pull = git.pull(); pull.setCredentialsProvider(getCredentialsProvider()); PullResult pullResult = pull.call(); LogHelper.log(new Status(IStatus.INFO, Activator.PI_GIT, 1, "Pull (fetch/merge) result " + pullResult.getFetchResult().getMessages() + "/" + pullResult.getMergeResult().getMergeStatus() + " for " + this, null)); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, e.getMessage(), e)); } finally { if (transport != null) transport.close(); } } private void rm() throws CoreException { // TODO: use org.eclipse.jgit.api.RmCommand, see Enhancement 379 if (!isRoot()) { try { Repository local = getLocalRepo(); Git git = new Git(local); CommitCommand commit = git.commit(); commit.setAll(true); commit.setMessage("auto-commit of " + toString()); commit.call(); } catch (Exception e) { throw new CoreException( new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, e.getMessage(), e)); } push(); } // else {cannot commit/push root removal} } private void commit(boolean dir) throws CoreException { try { Repository local = getLocalRepo(); Git git = new Git(local); String folderPattern = null; String filePattern = null; if (dir) { // add empty dir - http://stackoverflow.com/questions/115983/how-do-i-add-an-empty-directory-to-a-git-repository File folder = getLocalFile(); File gitignoreFile = new File(folder.getPath() + "/" + Constants.DOT_GIT_IGNORE); // /<folder>/.gitignore gitignoreFile.createNewFile(); String query = getUrl().getQuery(); if (query.equals("/")) { // root filePattern = Constants.DOT_GIT_IGNORE; } else { folderPattern = new Path(query).toString().substring(1); // <folder>/ filePattern = folderPattern + "/" + Constants.DOT_GIT_IGNORE; // <folder>/.gitignore } } else { // /<folder>/<file> IPath f = new Path(getUrl().getQuery()).removeLastSegments(1); // /<folder>/ String s = f.toString().substring(1); // <folder>/ folderPattern = s.equals("") ? null : s; // /<folder>/<file> s = getUrl().getQuery().substring(1); filePattern = s; } // TODO: folder may already exist, no need to add it again AddCommand add = git.add(); if (folderPattern != null) { add.addFilepattern(folderPattern); } add.addFilepattern(filePattern); add.call(); CommitCommand commit = git.commit(); commit.setMessage("auto-commit of " + this); commit.call(); LogHelper.log(new Status(IStatus.INFO, Activator.PI_GIT, 1, "Auto-commit of " + this + " done.", null)); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PI_GIT, IStatus.ERROR, e.getMessage(), e)); } } }