From a40fec7b8f9b8e3e09cbd41385523d855c20223c Mon Sep 17 00:00:00 2001 From: Brian Long Date: Mon, 7 Dec 2020 14:46:57 -0500 Subject: [PATCH] initial checkin --- .gitignore | 5 + pom.xml | 74 +++++ src/jetty/log4j2.xml | 16 ++ src/main/java/me/brianlong/git/CachedGit.java | 29 ++ .../me/brianlong/git/CompositeIterator.java | 42 +++ .../me/brianlong/git/CredentialedGit.java | 53 ++++ .../me/brianlong/git/ExpiringMapListener.java | 9 + .../java/me/brianlong/git/ExtendedGit.java | 139 ++++++++++ .../me/brianlong/git/LRUExpiringHashMap.java | 252 ++++++++++++++++++ .../java/me/brianlong/git/ListeningMap.java | 9 + .../brianlong/git/LocalRepositoryCache.java | 186 +++++++++++++ .../java/me/brianlong/git/MapListener.java | 15 ++ .../git/UniquePriorityFifoQueue.java | 236 ++++++++++++++++ .../me/brianlong/git/CommandUnitTest.java | 54 ++++ .../git/LocalRepositoryCacheUnitTest.java | 127 +++++++++ src/test/resources/log4j2.xml | 19 ++ 16 files changed, 1265 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/jetty/log4j2.xml create mode 100644 src/main/java/me/brianlong/git/CachedGit.java create mode 100644 src/main/java/me/brianlong/git/CompositeIterator.java create mode 100644 src/main/java/me/brianlong/git/CredentialedGit.java create mode 100644 src/main/java/me/brianlong/git/ExpiringMapListener.java create mode 100644 src/main/java/me/brianlong/git/ExtendedGit.java create mode 100644 src/main/java/me/brianlong/git/LRUExpiringHashMap.java create mode 100644 src/main/java/me/brianlong/git/ListeningMap.java create mode 100644 src/main/java/me/brianlong/git/LocalRepositoryCache.java create mode 100644 src/main/java/me/brianlong/git/MapListener.java create mode 100644 src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java create mode 100644 src/test/java/me/brianlong/git/CommandUnitTest.java create mode 100644 src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java create mode 100644 src/test/resources/log4j2.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc9b8bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Maven +target +.project +.classpath +.settings diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9f8dba4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + me.brianlong + git-utils + jar + 1.0-SNAPSHOT + + Git Utilities + + + 1.8 + 1.8 + UTF-8 + + + + + org.eclipse.jgit + org.eclipse.jgit + 5.9.0.202009080501-r + + + org.eclipse.jgit + org.eclipse.jgit.ssh.jsch + 5.9.0.202009080501-r + + + junit + junit + 4.13 + test + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.13.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT.class + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-tests + integration-test + + integration-test + + + + **/*IT.class + + + + + + + + diff --git a/src/jetty/log4j2.xml b/src/jetty/log4j2.xml new file mode 100644 index 0000000..ba7d307 --- /dev/null +++ b/src/jetty/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/java/me/brianlong/git/CachedGit.java b/src/main/java/me/brianlong/git/CachedGit.java new file mode 100644 index 0000000..0ad4815 --- /dev/null +++ b/src/main/java/me/brianlong/git/CachedGit.java @@ -0,0 +1,29 @@ +package me.brianlong.git; + +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.TransportException; +import org.eclipse.jgit.lib.Repository; + +public class CachedGit extends Git { + + public CachedGit(Git git) { + super(git.getRepository()); + } + + public CachedGit(CloneCommand clone) throws TransportException, InvalidRemoteException, GitAPIException { + super(clone.call().getRepository()); + } + + public CachedGit(Repository repo) { + super(repo); + } + + @Override + public void close() { + LocalRepositoryCache.getInstance().release(this); + } + +} diff --git a/src/main/java/me/brianlong/git/CompositeIterator.java b/src/main/java/me/brianlong/git/CompositeIterator.java new file mode 100644 index 0000000..860994b --- /dev/null +++ b/src/main/java/me/brianlong/git/CompositeIterator.java @@ -0,0 +1,42 @@ +package me.brianlong.git; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; + +public class CompositeIterator implements Iterator { + + private final List> iterators = new LinkedList>(); + private final Iterator> iteratorIterators; + private Iterator iterator; + + public CompositeIterator(@SuppressWarnings("unchecked") Collection... cs) { + for (Collection c : cs) + if (c != null && !c.isEmpty()) + this.iterators.add(c.iterator()); + this.iteratorIterators = this.iterators.iterator(); + } + + @Override + public boolean hasNext() { + if (this.iterator == null) { + if (!this.iteratorIterators.hasNext()) + return false; + this.iterator = this.iteratorIterators.next(); + } + + while (!this.iterator.hasNext() && this.iteratorIterators.hasNext()) + this.iterator = this.iteratorIterators.next(); + return this.iterator.hasNext(); + } + + @Override + public E next() { + if (!this.hasNext()) + throw new NoSuchElementException(); + return this.iterator.next(); + } + +} diff --git a/src/main/java/me/brianlong/git/CredentialedGit.java b/src/main/java/me/brianlong/git/CredentialedGit.java new file mode 100644 index 0000000..a5020a1 --- /dev/null +++ b/src/main/java/me/brianlong/git/CredentialedGit.java @@ -0,0 +1,53 @@ +package me.brianlong.git; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.FetchCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.LsRemoteCommand; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.PushCommand; +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; + +public class CredentialedGit extends ExtendedGit { + + private CredentialsProvider credProvider; + + public CredentialedGit(CloneCommand clone, CredentialsProvider creds) throws TransportException, InvalidRemoteException, GitAPIException { + this(clone.setCredentialsProvider(creds).call(), creds); + } + + public CredentialedGit(Git git, CredentialsProvider creds) { + super(git.getRepository()); + this.credProvider = creds; + } + + public CredentialedGit(Repository repo, CredentialsProvider creds) { + super(repo); + this.credProvider = creds; + } + + @Override + public FetchCommand fetch() { + return super.fetch().setCredentialsProvider(this.credProvider); + } + + @Override + public LsRemoteCommand lsRemote() { + return super.lsRemote().setCredentialsProvider(this.credProvider); + } + + @Override + public PullCommand pull() { + return super.pull().setCredentialsProvider(this.credProvider); + } + + @Override + public PushCommand push() { + return super.push().setCredentialsProvider(this.credProvider); + } + +} diff --git a/src/main/java/me/brianlong/git/ExpiringMapListener.java b/src/main/java/me/brianlong/git/ExpiringMapListener.java new file mode 100644 index 0000000..08802a4 --- /dev/null +++ b/src/main/java/me/brianlong/git/ExpiringMapListener.java @@ -0,0 +1,9 @@ +package me.brianlong.git; + +import java.util.Map.Entry; + +public interface ExpiringMapListener extends MapListener { + + void expired(Entry entry); + +} diff --git a/src/main/java/me/brianlong/git/ExtendedGit.java b/src/main/java/me/brianlong/git/ExtendedGit.java new file mode 100644 index 0000000..5fba65d --- /dev/null +++ b/src/main/java/me/brianlong/git/ExtendedGit.java @@ -0,0 +1,139 @@ +package me.brianlong.git; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.ListBranchCommand.ListMode; +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.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ExtendedGit extends CachedGit { + + private final Logger logger = LoggerFactory.getLogger(ExtendedGit.class); + private final Pattern gitUrlPattern = Pattern.compile("(((ssh|http(s)?)://([^@]+@)?([^:/]+)(:[0-9]+)?/)|git@([^:]+):)([\\w\\.@\\:/\\-~]+)(\\.git)"); + + public ExtendedGit(Git git) { + super(git.getRepository()); + } + + public ExtendedGit(CloneCommand clone) throws TransportException, InvalidRemoteException, GitAPIException { + super(clone.call().getRepository()); + } + + public ExtendedGit(Repository repo) { + 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()) + this.logger.debug("Remote URL: " + gitUrl); + + Matcher matcher = this.gitUrlPattern.matcher(gitUrl); + if (!matcher.find()) + throw new URISyntaxException(gitUrl, "The Git URL does not match the expected regular expression: " + this.gitUrlPattern.toString()); + return matcher.group(9); + } + + /** + * This method retrieves all branches, but excludes remote branches that are tracked with a local branch. + * @return + */ + public List getAllUniqueBranches() throws GitAPIException { + return this.getAllUniqueBranches(null); + } + + /** + * This method retrieves all branches, but excludes remote branches that are tracked with a local branch. + * @return + */ + public List getAllUniqueBranches(String containsCommitish) throws GitAPIException { + if (this.logger.isTraceEnabled()) + this.logger.trace("getAllUniqueBranches('" + containsCommitish + "')"); + + ListBranchCommand command = this.branchList().setListMode(ListMode.ALL); + if (containsCommitish != null) + command.setContains(containsCommitish); + List branches = command.call(); + + if (this.logger.isTraceEnabled()) + this.logger.trace("getAllUniqueBranches('" + containsCommitish + "'): branch count: " + branches.size()); + + Set abbrevBranchNames = new HashSet(branches.size()); + List trimmedBranches = new ArrayList(branches.size()); + + // sort them by local, then remote; keep order the same otherwise + Collections.sort(branches, new Comparator() { + @Override + public int compare(Ref o1, Ref o2) { + if (o1 == null) return -1; + else if (o2 == null) return 1; + else { + boolean o1local = o1.getName().startsWith(Constants.R_HEADS); + boolean o2local = o2.getName().startsWith(Constants.R_HEADS); + if (o1local && o2local) return 0; + else if (o1local) return -1; + else if (o2local) return 1; + else return 0; + } + } + }); + + if (this.logger.isTraceEnabled()) + this.logger.trace("getAllUniqueBranches('" + containsCommitish + "'): sorted"); + + for (Ref branch : branches) { + String fqBranchName = branch.getName(); + + String abbrevBranchName = null; + if (fqBranchName.startsWith(Constants.R_REMOTES)) { + if (this.logger.isDebugEnabled()) + this.logger.debug("Ref is a remote branch: " + fqBranchName); + abbrevBranchName = this.getRepository().shortenRemoteBranchName(fqBranchName); + } else if (fqBranchName.startsWith(Constants.R_HEADS)) { + if (this.logger.isDebugEnabled()) + this.logger.debug("Ref is a local branch: " + fqBranchName); + abbrevBranchName = Repository.shortenRefName(fqBranchName); + } else { + if (this.logger.isDebugEnabled()) + this.logger.debug("Ref is not a local or remote branch: " + fqBranchName); + } + + if (abbrevBranchName != null) { + if (abbrevBranchNames.contains(abbrevBranchName)) { + if (this.logger.isDebugEnabled()) + this.logger.debug("Branch already found; ignoring ..."); + } else { + abbrevBranchNames.add(abbrevBranchName); + trimmedBranches.add(branch); + } + } + } + + if (this.logger.isTraceEnabled()) + this.logger.trace("getAllUniqueBranches('" + containsCommitish + "'): " + trimmedBranches); + + return trimmedBranches; + } + +} diff --git a/src/main/java/me/brianlong/git/LRUExpiringHashMap.java b/src/main/java/me/brianlong/git/LRUExpiringHashMap.java new file mode 100644 index 0000000..08f4211 --- /dev/null +++ b/src/main/java/me/brianlong/git/LRUExpiringHashMap.java @@ -0,0 +1,252 @@ +package me.brianlong.git; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// TODO need to implement the actual expiration schedule +public class LRUExpiringHashMap implements ListeningMap { + + private final Logger logger = LoggerFactory.getLogger(LRUExpiringHashMap.class); + + private final Map map; + private final List> listeners = new LinkedList>(); + private final long expirationTimeMillis; + + public LRUExpiringHashMap(int expirationTimeInMinutes) { + this.map = new LinkedHashMap(); + this.expirationTimeMillis = expirationTimeInMinutes * 60L + 1000L; + } + + public LRUExpiringHashMap(int expirationTimeInMinutes, int 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.listeners.add(listener); + } + + @Override + public boolean containsKey(Object key) { + return this.map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.map.containsValue(value); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @Override + public Collection values() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public int size() { + return this.map.size(); + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) { + 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 + V value = this.map.remove(key); + this.map.put(new ExpiringHashKey((K)key, System.currentTimeMillis() + this.expirationTimeMillis), value); + + for (MapListener listener : this.listeners) + listener.accessed(new ExpiringHashMapEntry((K)key, value)); + return value; + } + + @Override + public V put(K key, V value) { + ExpiringHashKey ehkey = new ExpiringHashKey(key, System.currentTimeMillis() + this.expirationTimeMillis); + V oldValue = this.map.put(ehkey, value); + for (MapListener listener : this.listeners) + listener.added(new ExpiringHashMapEntry(key, value)); + return oldValue; + } + + @Override + public void clear() { + for (Entry entry : this.map.entrySet()) { + for (MapListener listener : this.listeners) + listener.cleared(new ExpiringHashMapEntry(entry.getKey().getKey(), entry.getValue())); + } + + this.map.clear(); + } + + @SuppressWarnings("unchecked") + @Override + public V remove(Object key) { + if (!this.map.containsKey(key)) + return null; + + V value = this.map.remove(key); + for (MapListener listener : this.listeners) + listener.removed(new ExpiringHashMapEntry((K)key, value)); + return value; + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + return this.map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this.map.equals(obj); + } + + 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())); + } + } + } + + public void expire(K key) { + if (this.logger.isDebugEnabled()) + this.logger.debug("expiring key from map: " + key); + + if (!this.map.containsKey(key)) + return; + + V value = this.map.remove(key); + for (MapListener listener : this.listeners) + if (listener instanceof ExpiringMapListener) + ((ExpiringMapListener)listener).expired(new ExpiringHashMapEntry(key, value)); + } + + + + private class ExpiringHashMapEntry implements Entry { + + private final K key; + private V value; + + public ExpiringHashMapEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return this.key; + } + + @Override + public V getValue() { + return this.value; + } + + @Override + public V setValue(V value) { + return this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Entry) { + return this.key.equals(((Entry)obj).getKey()) && this.value.equals(((Entry)obj).getValue()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return this.key.hashCode() + this.value.hashCode(); + } + + @Override + public String toString() { + return "{" + this.key + ", " + this.value + "}"; + } + + } + + private class ExpiringHashKey implements Serializable { + + private static final long serialVersionUID = -6511298315143655313L; + + private long expirationTimeInMillis; + private K key; + + public ExpiringHashKey(K key, long expirationTimeInMillis) { + this.key = key; + this.expirationTimeInMillis = expirationTimeInMillis; + } + + public K getKey() { + return this.key; + } + + 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); + } + } + + } + +} diff --git a/src/main/java/me/brianlong/git/ListeningMap.java b/src/main/java/me/brianlong/git/ListeningMap.java new file mode 100644 index 0000000..f3cbf53 --- /dev/null +++ b/src/main/java/me/brianlong/git/ListeningMap.java @@ -0,0 +1,9 @@ +package me.brianlong.git; + +import java.util.Map; + +public interface ListeningMap extends Map { + + void addListener(MapListener listener); + +} diff --git a/src/main/java/me/brianlong/git/LocalRepositoryCache.java b/src/main/java/me/brianlong/git/LocalRepositoryCache.java new file mode 100644 index 0000000..b72db2c --- /dev/null +++ b/src/main/java/me/brianlong/git/LocalRepositoryCache.java @@ -0,0 +1,186 @@ +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/main/java/me/brianlong/git/MapListener.java b/src/main/java/me/brianlong/git/MapListener.java new file mode 100644 index 0000000..47298f9 --- /dev/null +++ b/src/main/java/me/brianlong/git/MapListener.java @@ -0,0 +1,15 @@ +package me.brianlong.git; + +import java.util.Map.Entry; + +public interface MapListener { + + void added(Entry entry); + + void accessed(Entry entry); + + void removed(Entry entry); + + void cleared(Entry entry); + +} diff --git a/src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java b/src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java new file mode 100644 index 0000000..40ab2a6 --- /dev/null +++ b/src/main/java/me/brianlong/git/UniquePriorityFifoQueue.java @@ -0,0 +1,236 @@ +package me.brianlong.git; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.TreeSet; + +public class UniquePriorityFifoQueue implements Queue { + + private final Counter counter = new SimpleCounter(); + private final Counter fakeCounter = new FakeCounter(); + private final TreeSet> elements; + + public UniquePriorityFifoQueue() { + this.elements = new TreeSet>(); + } + + public UniquePriorityFifoQueue(Collection c) { + this.elements = new TreeSet>(); + for (T element : c) + this.elements.add(new PriorityFifoElement(element, this.counter)); + } + + public UniquePriorityFifoQueue(final Comparator comparator) { + this.elements = new TreeSet>(new Comparator>() { + @Override + public int compare(PriorityFifoElement o1, PriorityFifoElement o2) { + int compare = comparator.compare(o1.element, o2.element); + if (compare != 0) return compare; + return o1.compareTo(o2); + } + }); + } + + @Override + public boolean add(T e) { + return this.elements.add(new PriorityFifoElement(e, this.counter)); + } + + @Override + public boolean addAll(Collection c) { + if (c != null) for (T e : c) + this.elements.add(new PriorityFifoElement(e, this.counter)); + return true; + } + + @Override + public void clear() { + this.elements.clear(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean contains(Object o) { + return this.elements.contains(new PriorityFifoElement((T)o, this.fakeCounter)); + } + + @Override + public boolean containsAll(Collection c) { + if (c != null) for (Object o : c) + if (!this.contains(o)) + return false; + return true; + } + + @Override + public T element() { + return this.elements.iterator().next().element; + } + + @Override + public boolean isEmpty() { + return this.elements.isEmpty(); + } + + @Override + public Iterator iterator() { + final Iterator> i = this.elements.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public T next() { + return i.next().element; + } + + @Override + public void remove() { + i.remove(); + } + }; + } + + @Override + public boolean offer(T e) { + return this.elements.add(new PriorityFifoElement(e, this.counter)); + } + + @Override + public T peek() { + Iterator i = this.iterator(); + return i.hasNext() ? i.next() : null; + } + + @Override + public T poll() { + PriorityFifoElement element = this.elements.pollFirst(); + return element == null ? null : element.element; + } + + @Override + public T remove() { + PriorityFifoElement element = this.elements.pollFirst(); + if (element == null) + throw new NoSuchElementException(); + return element.element; + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(Object o) { + return this.elements.remove(new PriorityFifoElement((T)o, this.fakeCounter)); + } + + @Override + public boolean removeAll(Collection c) { + if (c != null) for (Object o : c) + this.remove(o); + return true; + } + + @Override + public boolean retainAll(Collection c) { + Iterator> i = this.elements.iterator(); + while (i.hasNext()) { + T e = i.next().element; + if (!c.contains(e)) + i.remove(); + } + + return true; + } + + @Override + public int size() { + return this.elements.size(); + } + + @SuppressWarnings("unchecked") + @Override + public Object[] toArray() { + Object[] objs = this.elements.toArray(); + Object[] ts = new Object[objs.length]; + for (int i = 0; i < objs.length; i++) + ts[i] = ((PriorityFifoElement)objs[i]).element; + return ts; + } + + @SuppressWarnings("unchecked") + public U[] toArray(U[] a) { + if (a.length < this.elements.size()) + a = Arrays.copyOf(a, this.elements.size()); + + int i = 0; + for (PriorityFifoElement element : this.elements) + a[i] = (U)element.element; + return a; + } + + + + private class PriorityFifoElement implements Comparable> { + + private final E element; + private final int fifoId; + + public PriorityFifoElement(E element, Counter counter) { + this.element = element; + this.fifoId = counter.next(); + } + + @Override + public int compareTo(PriorityFifoElement o) { + if (o == null) return 1; + else return this.fifoId - o.fifoId; + } + + @Override + public int hashCode() { + return this.element.hashCode(); + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (obj instanceof PriorityFifoElement) { + return this.element.equals(((PriorityFifoElement)obj).element); + } else { + return false; + } + } + + } + + private interface Counter { + + int next(); + + } + + public class FakeCounter implements Counter { + + public int next() { + return 0; + } + + } + + public class SimpleCounter implements Counter { + + private int count = 0; + + public synchronized int next() { + this.count++; + return this.count; + } + + } + +} diff --git a/src/test/java/me/brianlong/git/CommandUnitTest.java b/src/test/java/me/brianlong/git/CommandUnitTest.java new file mode 100644 index 0000000..3d05f81 --- /dev/null +++ b/src/test/java/me/brianlong/git/CommandUnitTest.java @@ -0,0 +1,54 @@ +package me.brianlong.git; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import org.eclipse.jgit.api.CloneCommand; +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; +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; + + @BeforeClass + public static void init() throws GitAPIException, IOException { + 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) + .setDirectory(tmpdir) + .call(); + } + + @AfterClass + public static void cleanup() throws IOException { + git.close(); + + FileUtils.delete(tmpdir, FileUtils.RECURSIVE); + } + + @Test + public void lotsOfBranches() throws GitAPIException { + List remoteBranches = git.branchList().setListMode(ListMode.REMOTE).call(); + Assert.assertNotNull(remoteBranches); + Assert.assertTrue(remoteBranches.size() > 5); + } + +} diff --git a/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java b/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java new file mode 100644 index 0000000..6eb115e --- /dev/null +++ b/src/test/java/me/brianlong/git/LocalRepositoryCacheUnitTest.java @@ -0,0 +1,127 @@ +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 new file mode 100644 index 0000000..7a59c70 --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +