diff --git a/pom.xml b/pom.xml index fb9f1f7..ed42cdb 100644 --- a/pom.xml +++ b/pom.xml @@ -2,10 +2,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - me.brianlong + com.inteligr8 git-utils jar - 1.0.0 + 1.1.0 Git Utilities diff --git a/src/main/java/me/brianlong/git/CachedGit.java b/src/main/java/com/inteligr8/git/CachedGit.java similarity index 78% rename from src/main/java/me/brianlong/git/CachedGit.java rename to src/main/java/com/inteligr8/git/CachedGit.java index 0ad4815..001b346 100644 --- a/src/main/java/me/brianlong/git/CachedGit.java +++ b/src/main/java/com/inteligr8/git/CachedGit.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; @@ -21,6 +21,10 @@ public class CachedGit extends Git { super(repo); } + public String getFirstRemoteUrl() throws GitAPIException { + return this.remoteList().call().iterator().next().getURIs().iterator().next().toString(); + } + @Override public void close() { LocalRepositoryCache.getInstance().release(this); diff --git a/src/main/java/me/brianlong/git/CompositeIterator.java b/src/main/java/com/inteligr8/git/CompositeIterator.java similarity index 97% rename from src/main/java/me/brianlong/git/CompositeIterator.java rename to src/main/java/com/inteligr8/git/CompositeIterator.java index 860994b..49beefe 100644 --- a/src/main/java/me/brianlong/git/CompositeIterator.java +++ b/src/main/java/com/inteligr8/git/CompositeIterator.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.util.Collection; import java.util.Iterator; diff --git a/src/main/java/me/brianlong/git/CredentialedGit.java b/src/main/java/com/inteligr8/git/CredentialedGit.java similarity index 98% rename from src/main/java/me/brianlong/git/CredentialedGit.java rename to src/main/java/com/inteligr8/git/CredentialedGit.java index a5020a1..67c7ea6 100644 --- a/src/main/java/me/brianlong/git/CredentialedGit.java +++ b/src/main/java/com/inteligr8/git/CredentialedGit.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.FetchCommand; diff --git a/src/main/java/com/inteligr8/git/DeveloperException.java b/src/main/java/com/inteligr8/git/DeveloperException.java new file mode 100644 index 0000000..906c20d --- /dev/null +++ b/src/main/java/com/inteligr8/git/DeveloperException.java @@ -0,0 +1,15 @@ +package com.inteligr8.git; + +import org.slf4j.Logger; + +public class DeveloperException extends RuntimeException { + + private static final long serialVersionUID = 1600616227925437703L; + private static final String MESSAGE = "This is a bug that needs to be addressed by the developer"; + + public DeveloperException(Logger logger, Throwable cause) { + super(MESSAGE, cause); + logger.error(MESSAGE); + } + +} diff --git a/src/main/java/me/brianlong/git/ExpiringMapListener.java b/src/main/java/com/inteligr8/git/ExpiringMapListener.java similarity index 83% rename from src/main/java/me/brianlong/git/ExpiringMapListener.java rename to src/main/java/com/inteligr8/git/ExpiringMapListener.java index 08802a4..dfee830 100644 --- a/src/main/java/me/brianlong/git/ExpiringMapListener.java +++ b/src/main/java/com/inteligr8/git/ExpiringMapListener.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.util.Map.Entry; diff --git a/src/main/java/me/brianlong/git/ExtendedGit.java b/src/main/java/com/inteligr8/git/ExtendedGit.java similarity index 97% rename from src/main/java/me/brianlong/git/ExtendedGit.java rename to src/main/java/com/inteligr8/git/ExtendedGit.java index f9896ab..427f5fb 100644 --- a/src/main/java/me/brianlong/git/ExtendedGit.java +++ b/src/main/java/com/inteligr8/git/ExtendedGit.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.io.IOException; import java.net.URISyntaxException; @@ -47,10 +47,6 @@ public class ExtendedGit extends CachedGit { super(repo); } - public String getFirstRemoteUrl() throws GitAPIException { - return this.remoteList().call().iterator().next().getURIs().iterator().next().toString(); - } - public String getRepositoryFullyQualifiedName() throws GitAPIException, URISyntaxException { String gitUrl = this.getFirstRemoteUrl(); if (this.logger.isDebugEnabled()) diff --git a/src/main/java/me/brianlong/git/LRUExpiringHashMap.java b/src/main/java/com/inteligr8/git/LRUExpiringHashMap.java similarity index 58% rename from src/main/java/me/brianlong/git/LRUExpiringHashMap.java rename to src/main/java/com/inteligr8/git/LRUExpiringHashMap.java index 08f4211..d98b7b8 100644 --- a/src/main/java/me/brianlong/git/LRUExpiringHashMap.java +++ b/src/main/java/com/inteligr8/git/LRUExpiringHashMap.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.io.Serializable; import java.util.Collection; @@ -17,24 +17,25 @@ public class LRUExpiringHashMap implements ListeningM private final Logger logger = LoggerFactory.getLogger(LRUExpiringHashMap.class); - private final Map map; + private final Object sync = new Object(); + private final LinkedHashMap map; private final List> listeners = new LinkedList>(); private final long expirationTimeMillis; public LRUExpiringHashMap(int expirationTimeInMinutes) { - this.map = new LinkedHashMap(); + this.map = new LinkedHashMap<>(); this.expirationTimeMillis = expirationTimeInMinutes * 60L + 1000L; } public LRUExpiringHashMap(int expirationTimeInMinutes, int initialCapacity) { - this.map = new LinkedHashMap(initialCapacity); + this.map = new LinkedHashMap<>(initialCapacity); this.expirationTimeMillis = expirationTimeInMinutes * 60L + 1000L; } @Override public void addListener(MapListener listener) { if (this.logger.isDebugEnabled()) - this.logger.debug("adding listener"); + this.logger.debug("adding listener: " + listener.getClass()); this.listeners.add(listener); } @@ -76,47 +77,60 @@ public class LRUExpiringHashMap implements ListeningM @Override @SuppressWarnings("unchecked") public V get(Object key) { - if (!this.map.containsKey(key)) - return null; + ExpiringValue evalue; - // remove and put to move the entry to the end of the map; helping with finding expired entries - V value = this.map.remove(key); - this.map.put(new ExpiringHashKey((K)key, System.currentTimeMillis() + this.expirationTimeMillis), value); + synchronized (this.sync) { + if (!this.map.containsKey(key)) + return null; + + // remove and put to move the entry to the end of the map; helping with finding expired entries + evalue = this.map.remove(key); + this.map.put((K)key, new ExpiringValue(evalue.getValue(), System.currentTimeMillis() + this.expirationTimeMillis)); + } for (MapListener listener : this.listeners) - listener.accessed(new ExpiringHashMapEntry((K)key, value)); - return value; + listener.accessed(new ExpiringHashMapEntry((K)key, evalue.getValue())); + return evalue.getValue(); } @Override public V put(K key, V value) { - ExpiringHashKey ehkey = new ExpiringHashKey(key, System.currentTimeMillis() + this.expirationTimeMillis); - V oldValue = this.map.put(ehkey, value); + ExpiringValue evalue = new ExpiringValue(value, System.currentTimeMillis() + this.expirationTimeMillis); + ExpiringValue oldValue = this.map.put(key, evalue); for (MapListener listener : this.listeners) listener.added(new ExpiringHashMapEntry(key, value)); - return oldValue; + return oldValue == null ? null : oldValue.getValue(); } @Override public void clear() { - for (Entry entry : this.map.entrySet()) { - for (MapListener listener : this.listeners) - listener.cleared(new ExpiringHashMapEntry(entry.getKey().getKey(), entry.getValue())); - } + List entries = new LinkedList<>(); - this.map.clear(); + synchronized (this.sync) { + for (Entry entry : this.map.entrySet()) + entries.add(new ExpiringHashMapEntry(entry.getKey(), entry.getValue().getValue())); + this.map.clear(); + } + + for (ExpiringHashMapEntry entry : entries) + for (MapListener listener : this.listeners) + listener.cleared(entry); } @SuppressWarnings("unchecked") @Override public V remove(Object key) { - if (!this.map.containsKey(key)) - return null; + ExpiringValue evalue; + + synchronized (this.sync) { + if (!this.map.containsKey(key)) + return null; + evalue = this.map.remove(key); + } - V value = this.map.remove(key); for (MapListener listener : this.listeners) - listener.removed(new ExpiringHashMapEntry((K)key, value)); - return value; + listener.removed(new ExpiringHashMapEntry((K)key, evalue.getValue())); + return evalue.getValue(); } @Override @@ -135,28 +149,49 @@ public class LRUExpiringHashMap implements ListeningM } public void exipriationCheck() { - Iterator> i = this.map.entrySet().iterator(); - for (Entry entry = i.next(); i.hasNext(); entry = i.next()) { - if (entry.getKey().isExpired()) { - i.remove(); - for (MapListener listener : this.listeners) - if (listener instanceof ExpiringMapListener) - ((ExpiringMapListener)listener).expired(new ExpiringHashMapEntry(entry.getKey().getKey(), entry.getValue())); + // since these should be order, we could break out of this loop once we reach an unexpired entry + List entries = new LinkedList<>(); + + synchronized (this.sync) { + Iterator> i = this.map.entrySet().iterator(); + for (Entry entry = i.next(); i.hasNext(); entry = i.next()) { + if (entry.getValue().isExpired()) { + i.remove(); + entries.add(new ExpiringHashMapEntry(entry.getKey(), entry.getValue().getValue())); + } else { + // ordered map; ordered by time of entry + // since expiration timers are constant, we can skip looping + break; + } } } + + for (ExpiringHashMapEntry entry : entries) + for (MapListener listener : this.listeners) + if (listener instanceof ExpiringMapListener) + ((ExpiringMapListener)listener).expired(entry); } public void expire(K key) { if (this.logger.isDebugEnabled()) this.logger.debug("expiring key from map: " + key); - - if (!this.map.containsKey(key)) - return; + + ExpiringValue evalue; - V value = this.map.remove(key); + synchronized (this.sync) { + if (!this.map.containsKey(key)) + return; + evalue = this.map.remove(key); + } + for (MapListener listener : this.listeners) if (listener instanceof ExpiringMapListener) - ((ExpiringMapListener)listener).expired(new ExpiringHashMapEntry(key, value)); + ((ExpiringMapListener)listener).expired(new ExpiringHashMapEntry(key, evalue.getValue())); + } + + @Override + public String toString() { + return this.map.toString(); } @@ -207,44 +242,27 @@ public class LRUExpiringHashMap implements ListeningM } - private class ExpiringHashKey implements Serializable { - - private static final long serialVersionUID = -6511298315143655313L; + private class ExpiringValue { private long expirationTimeInMillis; - private K key; + private V value; - public ExpiringHashKey(K key, long expirationTimeInMillis) { - this.key = key; + public ExpiringValue(V value, long expirationTimeInMillis) { + this.value = value; this.expirationTimeInMillis = expirationTimeInMillis; } - public K getKey() { - return this.key; + public V getValue() { + return this.value; } public boolean isExpired() { return this.expirationTimeInMillis <= System.currentTimeMillis(); } - @Override - public int hashCode() { - return this.key.hashCode(); - } - @Override public String toString() { - return this.key.toString(); - } - - @Override - @SuppressWarnings("unchecked") - public boolean equals(Object obj) { - if (obj instanceof LRUExpiringHashMap.ExpiringHashKey) { - return this.key.equals(((ExpiringHashKey)obj).key); - } else { - return this.key.equals(obj); - } + return this.value.toString(); } } diff --git a/src/main/java/me/brianlong/git/ListeningMap.java b/src/main/java/com/inteligr8/git/ListeningMap.java similarity index 83% rename from src/main/java/me/brianlong/git/ListeningMap.java rename to src/main/java/com/inteligr8/git/ListeningMap.java index f3cbf53..bd47e6d 100644 --- a/src/main/java/me/brianlong/git/ListeningMap.java +++ b/src/main/java/com/inteligr8/git/ListeningMap.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.util.Map; diff --git a/src/main/java/com/inteligr8/git/LocalRepositoryCache.java b/src/main/java/com/inteligr8/git/LocalRepositoryCache.java new file mode 100644 index 0000000..1b1c07d --- /dev/null +++ b/src/main/java/com/inteligr8/git/LocalRepositoryCache.java @@ -0,0 +1,242 @@ +package com.inteligr8.git; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.Semaphore; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LocalRepositoryCache { + + private static final LocalRepositoryCache INSTANCE = new LocalRepositoryCache(); + + public static LocalRepositoryCache getInstance() { + return INSTANCE; + } + + + + private final Logger logger = LoggerFactory.getLogger(LocalRepositoryCache.class); + + private final LRUExpiringHashMap cachedGits = new LRUExpiringHashMap<>(30); + private final Map gitUrlSemaphores = new HashMap<>(); + final File cacheDirectory = new File(System.getProperty("java.io.tmpdir"), "git"); + private final int simultaneousProcessesPerGitRepo = 1; + + private LocalRepositoryCache() { + this.cacheDirectory.mkdir(); + this.cachedGits.addListener(new RepositoryCacheMapListener()); + } + + @Override + protected void finalize() throws Throwable { + try { + this.destroy(); + } finally { + super.finalize(); + } + } + + private void destroy() { + if (this.logger.isDebugEnabled()) + this.logger.debug("clearing local repo cache"); + this.clear(); + } + + public CachedGit acquire(String url) throws InterruptedException, InvalidRemoteException, RefNotFoundException, TransportException, GitAPIException { + return this.acquire(url, null, null); + } + + public CachedGit acquire(String url, CredentialsProvider creds) throws InterruptedException, InvalidRemoteException, RefNotFoundException, TransportException, GitAPIException { + return this.acquire(url, creds, null); + } + + public CachedGit acquire(String url, String branch) throws InterruptedException, InvalidRemoteException, RefNotFoundException, TransportException, GitAPIException { + return this.acquire(url, null, branch); + } + + /** + * + * @param url A URL for the Git Repository. + * @param creds A username/password for the Git Repository. + * @param branch A branch name of the branch in the Git Repository to start working on. + * @return A JGit object. + * @throws InterruptedException The JVM was interrupted while the thread was waiting for another request on the same Git Repository + * @throws InvalidRemoteException The URL refers to nothing or an invalid Git Repository + * @throws RefNotFoundException The branch does not exist in the Git Repository represented by the URL + * @throws TransportException A disk or network error or hiccup occurred. + * @throws GitAPIException A Git API, library, or protocol related issue occurred + */ + public CachedGit acquire(String url, CredentialsProvider creds, String branch) + throws InterruptedException, InvalidRemoteException, RefNotFoundException, TransportException, GitAPIException { + if (this.logger.isTraceEnabled()) + this.logger.trace("acquire('" + url + "', " + creds + ", '" + branch + "')"); + + Semaphore semaphore = null; + synchronized (this) { + semaphore = this.gitUrlSemaphores.get(url); + if (semaphore == null) + this.gitUrlSemaphores.put(url, semaphore = new Semaphore(this.simultaneousProcessesPerGitRepo)); + } + + semaphore.acquire(); + try { + CachedGit git = this.cachedGits.remove(url); + if (git == null) { + File gitRepoDirectory = new File(this.cacheDirectory, UUID.randomUUID().toString() + ".git"); + if (this.logger.isDebugEnabled()) + this.logger.debug("Git directory cache for clone: " + gitRepoDirectory); + + CloneCommand clone = new CloneCommand() + .setURI(url) + .setDirectory(gitRepoDirectory); + if (branch != null) + clone.setBranch(branch); + + if (this.logger.isDebugEnabled()) + this.logger.debug("Cloning Git repository: " + url); + git = creds != null ? new CredentialedGit(clone, creds) : new ExtendedGit(clone); + if (this.logger.isInfoEnabled()) + this.logger.info("Cloned Git Repository: " + ((ExtendedGit)git).getRepositoryFullyQualifiedName()); + } else { + if (this.logger.isDebugEnabled()) + this.logger.debug("reseting Git"); + git.reset().setMode(ResetType.HARD).call(); + + if (branch != null) { + if (this.logger.isDebugEnabled()) + this.logger.debug("switching Git branches: " + branch); + git.checkout().setName(branch).call(); + } + + if (this.logger.isDebugEnabled()) + this.logger.debug("updating Git branch: " + git.getRepository().getBranch()); + git.pull().call(); + } + + return git; + } catch (URISyntaxException use) { + semaphore.release(); + throw new DeveloperException(this.logger, use); + } catch (IOException ie) { + semaphore.release(); + throw new TransportException("A I/O issue occurred", ie); + } catch (GitAPIException gae) { + semaphore.release(); + throw gae; + } + } + + public void release(CachedGit git) { + if (this.logger.isTraceEnabled()) + this.logger.trace("release('" + git.getRepository().getIdentifier() + "')"); + + try { + String url = git.getFirstRemoteUrl(); + + synchronized (this) { + Semaphore semaphore = this.gitUrlSemaphores.get(url); + if (semaphore != null) + semaphore.release(); + } + + this.cachedGits.put(url, git); + } catch (GitAPIException gae) { + if (this.logger.isDebugEnabled()) + this.logger.debug("A Git repository was never released", gae); + this.logger.warn("A Git repository was never released, potentially blocking all future processing by this tool until a restart: " + git.toString()); + } + } + + public void clear() { + if (this.logger.isTraceEnabled()) + this.logger.trace("clear()"); + + this.cachedGits.clear(); + + for (Semaphore semaphore : this.gitUrlSemaphores.values()) + ; // FIXME interrupt all the threads; releasing semaphores will not be enough + this.gitUrlSemaphores.clear(); + } + + private void expunge(Git git) { + File gitDir = git.getRepository().getDirectory(); + File workingTreeDir = git.getRepository().getWorkTree(); + git.getRepository().close(); + + try { + if (this.logger.isDebugEnabled()) + this.logger.debug("deleting: " + gitDir); + FileUtils.delete(gitDir, FileUtils.RECURSIVE); + } catch (IOException ie) { + this.logger.warn("Failed to delete a git directory: " + gitDir); + if (this.logger.isDebugEnabled()) + this.logger.debug(ie.getMessage(), ie); + } + + try { + if (this.logger.isDebugEnabled()) + this.logger.debug("deleting: " + workingTreeDir); + FileUtils.delete(workingTreeDir, FileUtils.RECURSIVE); + } catch (IOException ie) { + this.logger.warn("Failed to delete a git directory: " + workingTreeDir); + if (this.logger.isDebugEnabled()) + this.logger.debug(ie.getMessage(), ie); + } + + if (this.logger.isInfoEnabled()) + this.logger.info("Deleted Git Repository: " + gitDir); + } + + + + private class RepositoryCacheMapListener implements ExpiringMapListener { + + private final Logger logger = LoggerFactory.getLogger(LocalRepositoryCache.class); + + @Override + public void accessed(Entry entry) { + } + + @Override + public void added(Entry entry) { + // a clean one or one returned after being previously removed + } + + @Override + public void expired(Entry entry ) { + if (this.logger.isTraceEnabled()) + this.logger.trace("expired('" + entry.getKey() + "', '" + entry.getValue().getRepository().getIdentifier() + "')"); + expunge(entry.getValue()); + } + + @Override + public void removed(Entry entry) { + // expected to be removed only temporarily...for use elsewhere; do not close + } + + @Override + public void cleared(Entry entry) { + if (this.logger.isTraceEnabled()) + this.logger.trace("cleared('" + entry.getKey() + "', '" + entry.getValue().getRepository().getIdentifier() + "')"); + expunge(entry.getValue()); + } + + } + +} diff --git a/src/main/java/me/brianlong/git/MapListener.java b/src/main/java/com/inteligr8/git/MapListener.java similarity index 88% rename from src/main/java/me/brianlong/git/MapListener.java rename to src/main/java/com/inteligr8/git/MapListener.java index 47298f9..bf09a67 100644 --- a/src/main/java/me/brianlong/git/MapListener.java +++ b/src/main/java/com/inteligr8/git/MapListener.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.util.Map.Entry; diff --git a/src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java b/src/main/java/com/inteligr8/git/UniquePriorityFifoQueue.java similarity index 99% rename from src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java rename to src/main/java/com/inteligr8/git/UniquePriorityFifoQueue.java index 5cb4981..c0aa0a0 100644 --- a/src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java +++ b/src/main/java/com/inteligr8/git/UniquePriorityFifoQueue.java @@ -1,4 +1,4 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.util.Arrays; import java.util.Collection; diff --git a/src/main/java/me/brianlong/git/LocalRepositoryCache.java b/src/main/java/me/brianlong/git/LocalRepositoryCache.java deleted file mode 100644 index b72db2c..0000000 --- a/src/main/java/me/brianlong/git/LocalRepositoryCache.java +++ /dev/null @@ -1,186 +0,0 @@ -package me.brianlong.git; - -import java.io.File; -import java.io.IOException; -import java.util.Map.Entry; -import java.util.UUID; - -import org.eclipse.jgit.api.CloneCommand; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.InvalidRemoteException; -import org.eclipse.jgit.api.errors.RefNotFoundException; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.util.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class LocalRepositoryCache { - - private static final LocalRepositoryCache INSTANCE = new LocalRepositoryCache(); - - public static LocalRepositoryCache getInstance() { - return INSTANCE; - } - - private final Logger logger = LoggerFactory.getLogger(LocalRepositoryCache.class); - - private final LRUExpiringHashMap cachedGits = new LRUExpiringHashMap(30); -// private final Map gitIdsToUrls = new HashMap(); - private final File cacheDirectory = new File(System.getProperty("java.io.tmpdir"), "git"); - - private LocalRepositoryCache() { - this.cacheDirectory.mkdir(); - this.cachedGits.addListener(new RepositoryCacheMapListener()); - } - - @Override - protected void finalize() throws Throwable { - try { - this.destroy(); - } finally { - super.finalize(); - } - } - - private void destroy() { - if (this.logger.isDebugEnabled()) - this.logger.debug("clearing local repo cache"); - this.clear(); - } - - public synchronized ExtendedGit acquire(String url) throws GitAPIException, InvalidRemoteException, RefNotFoundException { - return this.acquire(url, null, null); - } - - public synchronized ExtendedGit acquire(String url, CredentialsProvider creds) throws GitAPIException, InvalidRemoteException, RefNotFoundException { - return this.acquire(url, creds, null); - } - - public synchronized ExtendedGit acquire(String url, String branch) throws GitAPIException, InvalidRemoteException, RefNotFoundException { - return this.acquire(url, null, branch); - } - - public synchronized ExtendedGit acquire(String url, CredentialsProvider creds, String branch) throws GitAPIException, InvalidRemoteException, RefNotFoundException { - if (this.logger.isTraceEnabled()) - this.logger.trace("acquire('" + url + "', " + creds + ", '" + branch + "')"); - -// Git git = this.cachedGits.remove(url); -// if (git == null) { - if (this.logger.isDebugEnabled()) - this.logger.debug("creating temporary Git directory"); - File gitRepoDirectory = new File(this.cacheDirectory, UUID.randomUUID().toString() + ".git"); - - CloneCommand clone = new CloneCommand() - .setURI(url) - .setDirectory(gitRepoDirectory); - if (branch != null) - clone.setBranch(branch); - - if (this.logger.isDebugEnabled()) - this.logger.debug("cloning Git repository: " + url); - ExtendedGit git = creds != null ? new CredentialedGit(clone, creds) : new ExtendedGit(clone); - if (this.logger.isInfoEnabled()) - this.logger.info("Cloned Git Repository"); - return git; -// git = creds != null ? new CredentialedGit(clone, creds) : new CachedGit(clone); -// this.gitIdsToUrls.put(git.getRepository().getIdentifier(), url); -// } else { -// if (branch != null) { -// if (this.logger.isDebugEnabled()) -// this.logger.debug("switching Git branches: " + branch); -// git.checkout().setName(branch).call(); -// } -// -// if (this.logger.isDebugEnabled()) -// this.logger.debug("updating Git branch: " + branch); -// git.pull().call(); -// } - } - - public synchronized void release(Git git) { - if (this.logger.isTraceEnabled()) - this.logger.trace("release('" + git.getRepository().getIdentifier() + "')"); - -// String url = this.gitIdsToUrls.get(git.getRepository().getIdentifier()); -// this.cachedGits.put(url, git); - this.expunge(git); - } - - public synchronized void clear() { - if (this.logger.isTraceEnabled()) - this.logger.trace("clear()"); - -// this.cachedGits.clear(); - } - - private void expunge(Git git) { -// gitIdsToUrls.remove(git.getRepository().getIdentifier()); - - File gitDir = git.getRepository().getDirectory(); - File workingTreeDir = git.getRepository().getWorkTree(); - git.getRepository().close(); - - try { - if (this.logger.isDebugEnabled()) - this.logger.debug("deleting: " + gitDir); - FileUtils.delete(gitDir, FileUtils.RECURSIVE); - } catch (IOException ie) { - this.logger.warn("Failed to delete a git directory: " + gitDir); - if (this.logger.isDebugEnabled()) - this.logger.debug(ie.getMessage(), ie); - gitDir.deleteOnExit(); - } - - try { - if (this.logger.isDebugEnabled()) - this.logger.debug("deleting: " + workingTreeDir); - FileUtils.delete(workingTreeDir, FileUtils.RECURSIVE); - } catch (IOException ie) { - this.logger.warn("Failed to delete a git directory: " + workingTreeDir); - if (this.logger.isDebugEnabled()) - this.logger.debug(ie.getMessage(), ie); - workingTreeDir.deleteOnExit(); - } - - if (this.logger.isInfoEnabled()) - this.logger.info("Deleted Git Repository"); - } - - - - private class RepositoryCacheMapListener implements ExpiringMapListener { - - private final Logger logger = LoggerFactory.getLogger(LocalRepositoryCache.class); - - @Override - public void accessed(Entry entry) { - } - - @Override - public void added(Entry entry) { - // a clean one or one returned after being previously removed - } - - @Override - public void expired(Entry entry) { - if (this.logger.isTraceEnabled()) - this.logger.trace("expired('" + entry.getKey() + "', '" + entry.getValue().getRepository().getIdentifier() + "')"); - expunge(entry.getValue()); - } - - @Override - public void removed(Entry entry) { - // expected to be removed only temporarily...for use elsewhere; do not close - } - - @Override - public void cleared(Entry entry) { - if (this.logger.isTraceEnabled()) - this.logger.trace("cleared('" + entry.getKey() + "', '" + entry.getValue().getRepository().getIdentifier() + "')"); - expunge(entry.getValue()); - } - - } - -} diff --git a/src/test/java/me/brianlong/git/CommandUnitTest.java b/src/test/java/com/inteligr8/git/CommandUnitTest.java similarity index 64% rename from src/test/java/me/brianlong/git/CommandUnitTest.java rename to src/test/java/com/inteligr8/git/CommandUnitTest.java index 3d05f81..84be925 100644 --- a/src/test/java/me/brianlong/git/CommandUnitTest.java +++ b/src/test/java/com/inteligr8/git/CommandUnitTest.java @@ -1,7 +1,8 @@ -package me.brianlong.git; +package com.inteligr8.git; import java.io.File; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -10,8 +11,6 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.util.FileUtils; import org.junit.AfterClass; import org.junit.Assert; @@ -19,9 +18,6 @@ import org.junit.BeforeClass; import org.junit.Test; public class CommandUnitTest { - - private final static CredentialsProvider gitCreds = new UsernamePasswordCredentialsProvider( - System.getProperty("git.username"), System.getProperty("git.token")); private static File tmpdir; private static Git git; @@ -31,8 +27,7 @@ public class CommandUnitTest { tmpdir = new File(System.getProperty("java.io.tmpdir"), "git-" + UUID.randomUUID().toString() + ".tmp"); git = new CloneCommand() - .setURI("git@github.com:bmlong137/env-docker-adbp.git") - .setCredentialsProvider(gitCreds) + .setURI("git@bitbucket.org:inteligr8/git-utils.git") .setDirectory(tmpdir) .call(); } @@ -45,10 +40,17 @@ public class CommandUnitTest { } @Test - public void lotsOfBranches() throws GitAPIException { + public void listOfBranches() throws GitAPIException, IOException { List remoteBranches = git.branchList().setListMode(ListMode.REMOTE).call(); Assert.assertNotNull(remoteBranches); - Assert.assertTrue(remoteBranches.size() > 5); + Assert.assertTrue(remoteBranches.contains(git.getRepository().findRef("origin/develop"))); + Assert.assertTrue(remoteBranches.contains(git.getRepository().findRef("origin/stable"))); + + List localBranches = git.branchList().call(); + Assert.assertNotNull(localBranches); + Assert.assertEquals(1, localBranches.size()); + + Assert.assertTrue(Collections.disjoint(remoteBranches, localBranches)); } } diff --git a/src/test/java/com/inteligr8/git/LocalRepositoryCacheUnitTest.java b/src/test/java/com/inteligr8/git/LocalRepositoryCacheUnitTest.java new file mode 100644 index 0000000..ec398b2 --- /dev/null +++ b/src/test/java/com/inteligr8/git/LocalRepositoryCacheUnitTest.java @@ -0,0 +1,196 @@ +package com.inteligr8.git; + +import java.io.IOException; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class LocalRepositoryCacheUnitTest { + + private final static CredentialsProvider badCreds = new UsernamePasswordCredentialsProvider("not-a-valid-user", "not-a-valid-password"); + private final static CredentialsProvider gitCreds = new UsernamePasswordCredentialsProvider( + System.getProperty("bitbucket.inteligr8.username"), System.getProperty("bitbucket.inteligr8.token")); + + private int cachedFiles; + + @Before + public void compileStats() { + this.cachedFiles = LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length; + } + @After + public void cleanupAndValidate() { + LocalRepositoryCache.getInstance().clear(); + Assert.assertEquals(this.cachedFiles, LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length); + } + + /** + * Since "host-does-not-exist.com" does not exist, an UnknownHostException + * is thrown. It is wrapped inside a JGit TransportException. + */ + @Test(expected = TransportException.class) + public void tryBadHostRepo() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://host-does-not-exist.com/bmlong137/does-not-exist.git"); + } + + /** + * Since "does-not-exist" isn't a public repo, it requires authentication + * to even check to see if it is private. This causes a + * NoRemoteRepositoryException which is wrapped inside a JGit + * InvalidRemoteException. This is because SSH authentication makes this + * more like cacheNonExistentRepoAuth() than cacheNonExistentRepo(). + */ + @Test(expected = InvalidRemoteException.class) + public void tryNonExistentRepoViaSsh() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("git@github.com:bmlong137/does-not-exist.git"); + } + + /** + * Since "does-not-exist" isn't a public repo, it requires authentication + * to even check to see if it is private. This causes a JSchException + * which is wrapped inside a JGit TransportException. + */ + @Test(expected = TransportException.class) + public void tryNonExistentRepo() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/does-not-exist.git"); + } + + /** + * Since "does-not-exist" isn't a repo, a NoRemoteRepositoryException is + * thrown. It is wrapped inside a JGit InvalidRemoteException. + */ + @Test(expected = InvalidRemoteException.class) + public void tryNonExistentRepoAuth() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/does-not-exist.git", gitCreds); + } + + /** + * Invalid credentials and repo doesn't actually exist + */ + @Test(expected = InvalidRemoteException.class) + public void tryInvalidCredsOnNoRepo() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/does-not-exist.git", badCreds); + } + + /** + * Invalid credentials and public repo exists + */ + @Test + public void cacheInvalidCredsOnPublicRepo() throws GitAPIException, InterruptedException { + Git git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git", badCreds); + try { + this.validateGenericGitRepo(git); + } finally { + git.close(); + } + } + + /** + * Invalid credentials and private repo exists + */ + @Test(expected = TransportException.class) + public void tryInvalidCredsOnPrivateRepo() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/propagate-branches.git", badCreds); + } + + @Test + public void cachePublicRepo() throws GitAPIException, InterruptedException { + Git git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git"); + try { + this.validateGenericGitRepo(git); + } finally { + git.close(); + } + } + + @Test + public void cachePublicRepoViaSsh() throws GitAPIException, InterruptedException { + Git git = LocalRepositoryCache.getInstance().acquire("git@bitbucket.org:inteligr8/git-utils.git"); + try { + this.validateGenericGitRepo(git); + } finally { + git.close(); + } + } + + /** + * Since "propagate-branches" isn't a public repo, it requires authentication + * to even check to see if it is private. This causes a JSchException + * which is wrapped inside a JGit TransportException. + */ + @Test(expected = TransportException.class) + public void tryPrivateRepoUnauth() throws GitAPIException, InterruptedException { + LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/propagate-branches.git"); + } + + @Test + public void cachePrivateRepo() throws GitAPIException, InterruptedException { + Git git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/propagate-branches.git", gitCreds); + try { + this.validateGenericGitRepo(git); + } finally { + git.close(); + } + } + + @Test + public void cachePublicRepoBranch() throws GitAPIException, InterruptedException, IOException { + Git git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git", "stable"); + try { + this.validateGenericGitRepo(git); + + Assert.assertEquals("stable", git.getRepository().getBranch()); + } finally { + git.close(); + } + } + + @Test + public void cacheReuse() throws GitAPIException, InterruptedException, IOException { + Git git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git"); + String repoId = git.getRepository().getIdentifier(); + git.close(); + + Assert.assertEquals(this.cachedFiles+1, LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length); + Thread.sleep(1000L); + Assert.assertEquals(this.cachedFiles+1, LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length); + + git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git"); + try { + Assert.assertEquals(repoId, git.getRepository().getIdentifier()); + git.checkout().setName("develop").setStartPoint("origin/develop").setCreateBranch(true).call(); + Assert.assertEquals("develop", git.getRepository().getBranch()); + } finally { + git.close(); + } + + Assert.assertEquals(this.cachedFiles+1, LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length); + Thread.sleep(1000L); + Assert.assertEquals(this.cachedFiles+1, LocalRepositoryCache.getInstance().cacheDirectory.listFiles().length); + + git = LocalRepositoryCache.getInstance().acquire("https://bitbucket.org/inteligr8/git-utils.git", "stable"); + try { + Assert.assertEquals("stable", git.getRepository().getBranch()); + } finally { + git.close(); + } + } + + private void validateGenericGitRepo(Git git) throws GitAPIException { + Assert.assertNotNull(git); + + Repository repo = git.getRepository(); + Assert.assertTrue(repo.getDirectory().exists()); + Assert.assertTrue(repo.getWorkTree().exists()); + Assert.assertTrue(repo.getWorkTree().listFiles().length > 0); + } + +} diff --git a/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java b/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java deleted file mode 100644 index 6eb115e..0000000 --- a/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package me.brianlong.git; - -import java.io.IOException; - -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.InvalidRemoteException; -import org.eclipse.jgit.api.errors.TransportException; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Test; - -public class LocalRepositoryCacheUnitTest { - - private final static CredentialsProvider gitCreds = new UsernamePasswordCredentialsProvider( - System.getProperty("git.username"), System.getProperty("git.token")); - - @AfterClass - public static void cleanup() { - LocalRepositoryCache.getInstance().clear(); - } - - /** - * Since "host-does-not-exist.com" does not exist, an UnknownHostException - * is thrown. It is wrapped inside a JGit TransportException. - */ - @Test(expected = TransportException.class) - public void cacheBadHostRepo() throws GitAPIException { - LocalRepositoryCache.getInstance().acquire("https://host-does-not-exist.com/bmlong137/does-not-exist.git"); - } - - /** - * Since "does-not-exist" isn't a public repo, it requires authentication - * to even check to see if it is private. This causes a - * NoRemoteRepositoryException which is wrapped inside a JGit - * InvalidRemoteException. This is because SSH authentication makes this - * more like cacheNonExistentRepoAuth() than cacheNonExistentRepo(). - */ - @Test(expected = InvalidRemoteException.class) - public void cacheNonExistentRepoViaSsh() throws GitAPIException { - LocalRepositoryCache.getInstance().acquire("git@github.com:bmlong137/does-not-exist.git"); - } - - /** - * Since "does-not-exist" isn't a public repo, it requires authentication - * to even check to see if it is private. This causes a JSchException - * which is wrapped inside a JGit TransportException. - */ - @Test(expected = TransportException.class) - public void cacheNonExistentRepo() throws GitAPIException { - LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/does-not-exist.git"); - } - - /** - * Since "does-not-exist" isn't a repo, a NoRemoteRepositoryException is - * thrown. It is wrapped inside a JGit InvalidRemoteException. - */ - @Test(expected = InvalidRemoteException.class) - public void cacheNonExistentRepoAuth() throws GitAPIException { - LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/does-not-exist.git", gitCreds); - } - - @Test - public void cachePublicRepo() throws GitAPIException { - Git git = LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/maven-file-management.git"); - try { - this.validateGenericGitRepo(git); - } finally { - git.close(); - } - } - - @Test - public void cachePublicRepoViaSsh() throws GitAPIException { - Git git = LocalRepositoryCache.getInstance().acquire("git@github.com:bmlong137/maven-file-management.git"); - try { - this.validateGenericGitRepo(git); - } finally { - git.close(); - } - } - - /** - * Since "github-api" isn't a public repo, it requires authentication - * to even check to see if it is private. This causes a JSchException - * which is wrapped inside a JGit TransportException. - */ - @Test(expected = TransportException.class) - public void cachePrivateRepoUnauth() throws GitAPIException { - LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/github-api.git"); - } - - @Test - public void cachePrivateRepo() throws GitAPIException { - Git git = LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/github-api.git", gitCreds); - try { - this.validateGenericGitRepo(git); - } finally { - git.close(); - } - } - - @Test - public void cachePublicRepoBranch() throws GitAPIException, IOException { - Git git = LocalRepositoryCache.getInstance().acquire("https://github.com/bmlong137/maven-file-management.git", "master"); - try { - this.validateGenericGitRepo(git); - - Assert.assertEquals("master", git.getRepository().getBranch()); - } finally { - git.close(); - } - } - - private void validateGenericGitRepo(Git git) throws GitAPIException { - Assert.assertNotNull(git); - - Repository repo = git.getRepository(); - Assert.assertTrue(repo.getDirectory().exists()); - Assert.assertTrue(repo.getWorkTree().exists()); - Assert.assertTrue(repo.getWorkTree().listFiles().length > 0); - } - -} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml index 7a59c70..60d0dce 100644 --- a/src/test/resources/log4j2.xml +++ b/src/test/resources/log4j2.xml @@ -6,7 +6,7 @@ - +