Skip to content

Commit

Permalink
Merge pull request #236 from stephenc/jenkins-40834
Browse files Browse the repository at this point in the history
[JENKINS-40834] Add an API to resolve symbolic refs
  • Loading branch information
MarkEWaite committed Mar 24, 2017
2 parents 85f78b9 + f5e2296 commit 8550139
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 1 deletion.
33 changes: 32 additions & 1 deletion src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
Expand Up @@ -2611,7 +2611,38 @@ public Map<String, ObjectId> getRemoteReferences(String url, String pattern, boo
return references;
}

//
@Override
public Map<String, String> getRemoteSymbolicReferences(String url, String pattern)
throws GitException, InterruptedException {
Map<String, String> references = new HashMap<>();
if (isAtLeastVersion(2, 8, 0, 0)) {
// --symref is only understood by ls-remote starting from git 2.8.0
// https://github.com/git/git/blob/afd6726309/Documentation/RelNotes/2.8.0.txt#L72-L73
ArgumentListBuilder args = new ArgumentListBuilder("ls-remote");
args.add("--symref");
args.add(url);
if (pattern != null) {
args.add(pattern);
}

StandardCredentials cred = credentials.get(url);
if (cred == null) cred = defaultCredentials;

String result = launchCommandWithCredentials(args, null, cred, url);

String[] lines = result.split("\n");
Pattern symRefPattern = Pattern.compile("^ref:\\s+([^ ]+)\\s+([^ ]+)$");
for (String line : lines) {
Matcher matcher = symRefPattern.matcher(line);
if (matcher.matches()) {
references.put(matcher.group(2), matcher.group(1));
}
}
}
return references;
}

//
//
// Legacy Implementation of IGitAPI
//
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/jenkinsci/plugins/gitclient/GitClient.java
Expand Up @@ -619,6 +619,20 @@ public interface GitClient {
*/
Map<String, ObjectId> getRemoteReferences(String remoteRepoUrl, String pattern, boolean headsOnly, boolean tagsOnly) throws GitException, InterruptedException;

/**
* List symbolic references in a remote repository. Equivalent to <tt>git ls-remote --symref &lt;repository&gt;
* [&lt;refs&gt;]</tt>. Note: the response may be empty for multiple reasons
*
* @param remoteRepoUrl Remote repository URL.
* @param pattern Only references matching the given pattern are displayed.
* @return a map of references name and its underlying reference. Empty if none or if the remote does not report
* symbolic references (i.e. Git 1.8.4 or earlier) or if the client does not support reporting symbolic references
* (e.g. command line Git prior to 2.8.0).
* @throws hudson.plugins.git.GitException if underlying git operation fails.
* @throws java.lang.InterruptedException if interrupted.
*/
Map<String, String> getRemoteSymbolicReferences(String remoteRepoUrl, String pattern) throws GitException, InterruptedException;

/**
* Retrieve commit object that is direct child for <tt>revName</tt> revision reference.
*
Expand Down
67 changes: 67 additions & 0 deletions src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java
Expand Up @@ -30,7 +30,9 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -62,6 +64,7 @@
import org.eclipse.jgit.api.ShowNoteCommand;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
Expand All @@ -71,6 +74,7 @@
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.fnmatch.FileNameMatcher;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
Expand All @@ -85,6 +89,7 @@
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.revwalk.RevCommit;
Expand All @@ -97,6 +102,7 @@
import org.eclipse.jgit.revwalk.filter.MaxCountRevFilter;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.transport.BasePackFetchConnection;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchConnection;
import org.eclipse.jgit.transport.HttpTransport;
Expand Down Expand Up @@ -784,6 +790,67 @@ public Map<String, ObjectId> getRemoteReferences(String url, String pattern, boo
return references;
}

@Override
public Map<String, String> getRemoteSymbolicReferences(String url, String pattern)
throws GitException, InterruptedException {
Map<String, String> references = new HashMap<>();
String regexPattern = null;
if (pattern != null) {
regexPattern = createRefRegexFromGlob(pattern);
}
// HACK ALERT... JGit doesn't give the info we require hard-coding HEAD symref support only based on pre 1.8.5
// behaviour of the git command line - whereby it would just look for the branch with the same ref as HEAD
// and if there are multiple matches and one of them is master then we can assume master... otherwise
// throw our hands up and say "no clue"
if (regexPattern != null && !Constants.HEAD.matches(regexPattern)) {
return references;
}
try (Repository repo = openDummyRepository()) {
try {
// HACK HACK HACK
// The symref info is advertised as a capability starting from git 1.8.5
// So all we need to do is ask JGit to fetch the refs and then (because JGit adds all capabilities
// into a Set) we iterate the resulting set to find any that matching symref=$symref:$realref
// of course JGit does not expose a way to iterate the capabilities, so instead we have to hack
// and peek inside
// TODO if JGit implement https://bugs.eclipse.org/bugs/show_bug.cgi?id=514052 we should switch to that
Class<?> basePackConnection = BasePackFetchConnection.class.getSuperclass();
Field remoteCapablities = basePackConnection.getDeclaredField("remoteCapablities");
remoteCapablities.setAccessible(true);
try (Transport transport = Transport.open(repo, url)) {
transport.setCredentialsProvider(getProvider());
try (FetchConnection fc = transport.openFetch()) {
fc.getRefs();
if (fc instanceof BasePackFetchConnection) {
Object o = remoteCapablities.get(fc);
if (o instanceof Set) {
boolean hackWorked = false;
for (String capability: (Set<String>)o) {
if (capability.startsWith("symref=")) {
hackWorked = true;
int index = capability.indexOf(":", 7);
if (index != -1) {
references.put(capability.substring(7, index), capability.substring(index+1));
}
}
}
if (hackWorked) {
return references;
}
}
}
}
// ignore this is a total hack
}
} catch (IllegalAccessException | NoSuchFieldException e) {
// ignore, caller will just have to try it the Git 1.8.4 way, we'll return an empty map
}
} catch (IOException | URISyntaxException e) {
throw new GitException(e);
}
return references;
}

/* Adapted from http://stackoverflow.com/questions/1247772/is-there-an-equivalent-of-java-util-regex-for-glob-type-patterns */
private String createRefRegexFromGlob(String glob)
{
Expand Down
Expand Up @@ -547,6 +547,12 @@ public Map<String, ObjectId> getRemoteReferences(String remoteRepoUrl, String pa
return proxy.getRemoteReferences(remoteRepoUrl, pattern, headsOnly, tagsOnly);
}

/** {@inheritDoc} */
public Map<String, String> getRemoteSymbolicReferences(String remoteRepoUrl, String pattern)
throws GitException, InterruptedException {
return proxy.getRemoteSymbolicReferences(remoteRepoUrl, pattern);
}

/** {@inheritDoc} */
public ObjectId revParse(String revName) throws GitException, InterruptedException {
return proxy.revParse(revName);
Expand Down
Expand Up @@ -17,6 +17,11 @@ protected GitClient setupGitAPI(File ws) throws Exception {
return Git.with(listener, env).in(ws).using("git").getClient();
}

@Override
protected boolean hasWorkingGetRemoteSymbolicReferences() {
return ((CliGitAPIImpl)(w.git)).isAtLeastVersion(2,8,0,0);
}

private static boolean cliGitDefaultsSet = false;

private void setCliGitDefaults() throws Exception {
Expand Down
23 changes: 23 additions & 0 deletions src/test/java/org/jenkinsci/plugins/gitclient/GitAPITestCase.java
Expand Up @@ -6,6 +6,8 @@
import static org.hamcrest.Matchers.*;
import static org.jenkinsci.plugins.gitclient.StringSharesPrefix.sharesPrefix;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeThat;

import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
Expand Down Expand Up @@ -3372,6 +3374,27 @@ public void test_getRemoteReferences_withMatchingPattern() throws Exception {
assertTrue(references.isEmpty());
}

/**
* Test getRemoteSymbolicReferences with listing all references
*/
public void test_getRemoteSymbolicReferences() throws Exception {
assumeThat(hasWorkingGetRemoteSymbolicReferences(), is(true));
Map<String, String> references = w.git.getRemoteSymbolicReferences(remoteMirrorURL, null);
assertThat(references, hasEntry(is(Constants.HEAD), is(Constants.R_HEADS + Constants.MASTER)));
}

protected abstract boolean hasWorkingGetRemoteSymbolicReferences();

/**
* Test getRemoteSymbolicReferences with listing all references
*/
public void test_getRemoteSymbolicReferences_withMatchingPattern() throws Exception {
assumeThat(hasWorkingGetRemoteSymbolicReferences(), is(true));
Map<String, String> references = w.git.getRemoteSymbolicReferences(remoteMirrorURL, Constants.HEAD);
assertThat(references, hasEntry(is(Constants.HEAD), is(Constants.R_HEADS + Constants.MASTER)));
assertThat(references.size(), is(1));
}

private Properties parseLsRemote(File file) throws IOException
{
Properties properties = new Properties();
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/org/jenkinsci/plugins/gitclient/JGitAPIImplTest.java
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.eclipse.jgit.transport.BasePackFetchConnection;

/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
Expand All @@ -13,6 +14,17 @@ protected GitClient setupGitAPI(File ws) throws Exception {
return Git.with(listener, env).in(ws).using("jgit").getClient();
}

@Override
protected boolean hasWorkingGetRemoteSymbolicReferences() {
try {
// TODO if JGit implement https://bugs.eclipse.org/bugs/show_bug.cgi?id=514052 we should switch to that
BasePackFetchConnection.class.getSuperclass().getDeclaredField("remoteCapablities");
return true;
} catch (NoSuchFieldException e) {
return false;
}
}

/**
* timeout is not implemented in JGitAPIImpl.
*/
Expand Down
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.eclipse.jgit.transport.BasePackFetchConnection;

/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
Expand All @@ -13,6 +14,17 @@ protected GitClient setupGitAPI(File ws) throws Exception {
return Git.with(listener, env).in(ws).using("jgitapache").getClient();
}

@Override
protected boolean hasWorkingGetRemoteSymbolicReferences() {
try {
// TODO if JGit implement https://bugs.eclipse.org/bugs/show_bug.cgi?id=514052 we should switch to that
BasePackFetchConnection.class.getSuperclass().getDeclaredField("remoteCapablities");
return true;
} catch (NoSuchFieldException e) {
return false;
}
}

/**
* timeout is not implemented in JGitAPIImpl.
*/
Expand Down

0 comments on commit 8550139

Please sign in to comment.