From 178c9ff4d0f46c40db2bc6e4c3910ae0dd0d29d0 Mon Sep 17 00:00:00 2001 From: evanchooly Date: Mon, 4 Nov 2013 20:08:24 -0500 Subject: [PATCH] add support (most of) the release-related endpoints --- pom.xml | 6 +- src/main/java/org/kohsuke/github/GHAsset.java | 103 ++++++++++ .../java/org/kohsuke/github/GHRelease.java | 187 ++++++++++++++++++ .../org/kohsuke/github/GHReleaseBuilder.java | 75 +++++++ .../java/org/kohsuke/github/GHRepository.java | 9 + .../java/org/kohsuke/github/Requester.java | 39 +++- src/test/java/org/kohsuke/LifecycleTest.java | 146 ++++++++++++++ 7 files changed, 557 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/GHAsset.java create mode 100644 src/main/java/org/kohsuke/github/GHRelease.java create mode 100644 src/main/java/org/kohsuke/github/GHReleaseBuilder.java create mode 100644 src/test/java/org/kohsuke/LifecycleTest.java diff --git a/pom.xml b/pom.xml index f2f0831cdd..16645bb2ad 100644 --- a/pom.xml +++ b/pom.xml @@ -85,8 +85,12 @@ 1.1 test + + org.eclipse.jgit + org.eclipse.jgit + 3.1.0.201310021548-r + - repo.jenkins-ci.org diff --git a/src/main/java/org/kohsuke/github/GHAsset.java b/src/main/java/org/kohsuke/github/GHAsset.java new file mode 100644 index 0000000000..e70730aac4 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHAsset.java @@ -0,0 +1,103 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.util.Date; + +public class GHAsset { + GitHub root; + GHRepository owner; + private String url; + private String id; + private String name; + private String label; + private String state; + private String content_type; + private long size; + private long download_count; + private Date created_at; + private Date updated_at; + + public String getContentType() { + return content_type; + } + + public void setContentType(String contentType) throws IOException { + edit("content_type", contentType); + this.content_type = contentType; + } + + public Date getCreatedAt() { + return created_at; + } + + public long getDownloadCount() { + return download_count; + } + + public String getId() { + return id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) throws IOException { + edit("label", label); + this.label = label; + } + + public String getName() { + return name; + } + + public GHRepository getOwner() { + return owner; + } + + public GitHub getRoot() { + return root; + } + + public long getSize() { + return size; + } + + public String getState() { + return state; + } + + public Date getUpdatedAt() { + return updated_at; + } + + public String getUrl() { + return url; + } + + private void edit(String key, Object value) throws IOException { + new Requester(root)._with(key, value).method("PATCH").to(getApiRoute()); + } + + public void delete() throws IOException { + new Requester(root).method("DELETE").to(getApiRoute()); + } + + + private String getApiRoute() { + return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/releases/assets/" + id; + } + + GHAsset wrap(GHRelease release) { + this.owner = release.getOwner(); + this.root = owner.root; + return this; + } + + public static GHAsset[] wrap(GHAsset[] assets, GHRelease release) { + for (GHAsset aTo : assets) { + aTo.wrap(release); + } + return assets; + } +} diff --git a/src/main/java/org/kohsuke/github/GHRelease.java b/src/main/java/org/kohsuke/github/GHRelease.java new file mode 100644 index 0000000000..3efd04b11c --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHRelease.java @@ -0,0 +1,187 @@ +package org.kohsuke.github; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Date; + +import static java.lang.String.format; + +public class GHRelease { + GitHub root; + GHRepository owner; + + private String url; + private String html_url; + private String assets_url; + private String upload_url; + private long id; + private String tag_name; + private String target_commitish; + private String name; + private String body; + private boolean draft; + private boolean prerelease; + private Date created_at; + private Date published_at; + + public String getAssetsUrl() { + return assets_url; + } + + public void setAssetsUrl(String assets_url) { + this.assets_url = assets_url; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Date getCreatedAt() { + return created_at; + } + + public void setCreatedAt(Date created_at) { + this.created_at = created_at; + } + + public boolean isDraft() { + return draft; + } + + public void setDraft(boolean draft) { + this.draft = draft; + } + + public String getHtmlUrl() { + return html_url; + } + + public void setHtmlUrl(String html_url) { + this.html_url = html_url; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public GHRepository getOwner() { + return owner; + } + + public void setOwner(GHRepository owner) { + this.owner = owner; + } + + public boolean isPrerelease() { + return prerelease; + } + + public void setPrerelease(boolean prerelease) { + this.prerelease = prerelease; + } + + public Date getPublished_at() { + return published_at; + } + + public void setPublished_at(Date published_at) { + this.published_at = published_at; + } + + public GitHub getRoot() { + return root; + } + + public void setRoot(GitHub root) { + this.root = root; + } + + public String getTagName() { + return tag_name; + } + + public void setTagName(String tag_name) { + this.tag_name = tag_name; + } + + public String getTargetCommitish() { + return target_commitish; + } + + public void setTargetCommitish(String target_commitish) { + this.target_commitish = target_commitish; + } + + public String getUploadUrl() { + return upload_url; + } + + public void setUploadUrl(String upload_url) { + this.upload_url = upload_url; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + GHRelease wrap(GHRepository owner) { + this.owner = owner; + this.root = owner.root; + return this; + } + + static GHRelease[] wrap(GHRelease[] releases, GHRepository owner) { + for (GHRelease release : releases) { + release.wrap(owner); + } + return releases; + } + + /** + * Because github relies on SNI (http://en.wikipedia.org/wiki/Server_Name_Indication) this method will only work on + * Java 7 or greater. Options for fixing this for earlier JVMs can be found here + * http://stackoverflow.com/questions/12361090/server-name-indication-sni-on-java but involve more complicated + * handling of the HTTP requests to github's API. + * + * @throws IOException + */ + public GHAsset uploadAsset(File file, String contentType) throws IOException { + Requester builder = new Requester(owner.root); + + String url = format("https://uploads.github.com%sreleases/%d/assets?name=%s", + owner.getApiTailUrl(""), getId(), file.getName()); + return builder.contentType(contentType) + .with(new FileInputStream(file)) + .to(url, GHAsset.class).wrap(this); + } + + public GHAsset[] getAssets() throws IOException { + Requester builder = new Requester(owner.root); + + GHAsset[] assets = (GHAsset[]) builder + .method("GET") + .to(owner.getApiTailUrl(format("releases/%d/assets", id)), GHAsset[].class); + return GHAsset.wrap(assets, this); + } +} diff --git a/src/main/java/org/kohsuke/github/GHReleaseBuilder.java b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java new file mode 100644 index 0000000000..0b368dbb78 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java @@ -0,0 +1,75 @@ +package org.kohsuke.github; + +import java.io.IOException; + +public class GHReleaseBuilder { + private final GHRepository repo; + private final Requester builder; + + public GHReleaseBuilder(GHRepository ghRepository, String tag) { + this.repo = ghRepository; + this.builder = new Requester(repo.root); + builder.with("tag_name", tag); + } + + /** + * @param body The release notes body. + */ + public GHReleaseBuilder body(String body) { + if (body != null) { + builder.with("body", body); + } + return this; + } + + /** + * Specifies the commitish value that determines where the Git tag is created from. Can be any branch or + * commit SHA. + * + * @param commitish Defaults to the repository’s default branch (usually "master"). Unused if the Git tag + * already exists. + * @return + */ + public GHReleaseBuilder commitish(String commitish) { + if (commitish != null) { + builder.with("target_commitish", commitish); + } + return this; + } + + /** + * Optional. + * + * @param draft {@code true} to create a draft (unpublished) release, {@code false} to create a published one. + * Default is {@code false}. + */ + public GHReleaseBuilder draft(boolean draft) { + builder.with("draft", draft); + return this; + } + + /** + * @param name the name of the release + */ + public GHReleaseBuilder name(String name) { + if (name != null) { + builder.with("name", name); + } + return this; + } + + /** + * Optional + * + * @param prerelease {@code true} to identify the release as a prerelease. {@code false} to identify the release + * as a full release. Default is {@code false}. + */ + public GHReleaseBuilder prerelease(boolean prerelease) { + builder.with("prerelease", prerelease); + return this; + } + + public GHRelease create() throws IOException { + return builder.to(repo.getApiTailUrl("releases"), GHRelease.class).wrap(repo); + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 5302413c3a..93748037f8 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -146,6 +146,15 @@ public List getIssues(GHIssueState state) throws IOException { return Arrays.asList(GHIssue.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/issues?state=" + state.toString().toLowerCase(), GHIssue[].class), this)); } + public GHReleaseBuilder createRelease(String tag) { + return new GHReleaseBuilder(this,tag); + } + + public List getReleases() throws IOException { + return Arrays.asList(GHRelease.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/releases", + GHRelease[].class), this)); + } + protected String getOwnerName() { return owner.login; } diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 795c2e2a1e..fa9bc3191c 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -25,6 +25,7 @@ import org.apache.commons.io.IOUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -47,7 +48,7 @@ import java.util.Set; import java.util.zip.GZIPInputStream; -import static org.kohsuke.github.GitHub.*; +import static org.kohsuke.github.GitHub.MAPPER; /** * A builder pattern for making HTTP call and parsing its output. @@ -62,6 +63,8 @@ class Requester { * Request method. */ private String method = "POST"; + private String contentType = "application/x-www-form-urlencoded"; + private InputStream body; private static class Entry { String key; @@ -113,6 +116,11 @@ public Requester with(String key, Map value) { return _with(key, value); } + public Requester with(InputStream body) { + this.body = body; + return this; + } + public Requester _with(String key, Object value) { if (value!=null) { args.add(new Entry(key,value)); @@ -125,6 +133,11 @@ public Requester method(String method) { return this; } + public Requester contentType(String contentType) { + this.contentType = contentType; + return this; + } + public void to(String tailApiUrl) throws IOException { to(tailApiUrl,null); } @@ -162,13 +175,25 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti if (!method.equals("GET")) { uc.setDoOutput(true); - uc.setRequestProperty("Content-type","application/x-www-form-urlencoded"); + uc.setRequestProperty("Content-type", contentType); - Map json = new HashMap(); - for (Entry e : args) { - json.put(e.key, e.value); + if (body == null) { + Map json = new HashMap(); + for (Entry e : args) { + json.put(e.key, e.value); + } + MAPPER.writeValue(uc.getOutputStream(), json); + } else { + try { + byte[] bytes = new byte[32768]; + int read = 0; + while ((read = body.read(bytes)) != -1) { + uc.getOutputStream().write(bytes, 0, read); + } + } finally { + body.close(); + } } - MAPPER.writeValue(uc.getOutputStream(),json); } try { @@ -269,7 +294,7 @@ private void findNextURL(HttpURLConnection uc) throws MalformedURLException { private HttpURLConnection setupConnection(URL url) throws IOException { - HttpURLConnection uc = (HttpURLConnection) url.openConnection(); + HttpsURLConnection uc = (HttpsURLConnection) url.openConnection(); // if the authentication is needed but no credential is given, try it anyway (so that some calls // that do work with anonymous access in the reduced form should still work.) diff --git a/src/test/java/org/kohsuke/LifecycleTest.java b/src/test/java/org/kohsuke/LifecycleTest.java new file mode 100644 index 0000000000..f9e8b820ad --- /dev/null +++ b/src/test/java/org/kohsuke/LifecycleTest.java @@ -0,0 +1,146 @@ +package org.kohsuke; + +import junit.framework.TestCase; +import org.apache.commons.io.IOUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.kohsuke.github.GHAsset; +import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHMilestone; +import org.kohsuke.github.GHMyself; +import org.kohsuke.github.GHRelease; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Properties; + +public class LifecycleTest extends TestCase { + private GitHub gitHub; + + @Override + public void setUp() throws Exception { + super.setUp(); + gitHub = GitHub.connect(); + } + + public void testCreateRepository() throws IOException, GitAPIException { + GHMyself myself = gitHub.getMyself(); + GHRepository repository = myself.getRepository("github-api-test"); + if (repository != null) { + repository.delete(); + } + repository = gitHub.createRepository("github-api-test", + "a test repository used to test kohsuke's github-api", "http://github-api.kohsuke.org/", true); + + assertTrue(repository.getReleases().isEmpty()); + try { + GHMilestone milestone = repository.createMilestone("Initial Release", "first one"); + GHIssue issue = repository.createIssue("Test Issue") + .body("issue body just for grins") + .milestone(milestone) + .assignee(myself) + .label("bug") + .create(); + File repoDir = new File(System.getProperty("java.io.tmpdir"), "github-api-test"); + delete(repoDir); + Git origin = Git.cloneRepository() + .setBare(false) + .setURI(repository.gitHttpTransportUrl()) + .setDirectory(repoDir) + .setCredentialsProvider(getCredentialsProvider(myself)) + .call(); + + commitTestFile(myself, repoDir, origin); + + + GHRelease release = createRelease(repository); + + GHAsset asset = uploadAsset(release); + + updateAsset(release, asset); + + deleteAsset(release, asset); + } finally { + repository.delete(); + } + } + + private void updateAsset(GHRelease release, GHAsset asset) throws IOException { + asset.setLabel("test label"); + assertEquals("test label", release.getAssets()[0].getLabel()); + } + + private void deleteAsset(GHRelease release, GHAsset asset) throws IOException { + asset.delete(); + assertEquals(0, release.getAssets().length); + } + + private GHAsset uploadAsset(GHRelease release) throws IOException { + GHAsset asset = release.uploadAsset(new File("pom.xml"), "application/text"); + assertNotNull(asset); + GHAsset[] assets = release.getAssets(); + assertEquals(1, assets.length); + assertEquals("pom.xml", assets[0].getName()); + + return asset; + } + + private GHRelease createRelease(GHRepository repository) throws IOException { + GHRelease builder = repository.createRelease("release_tag") + .name("Test Release") + .body("How exciting! To be able to programmatically create releases is a dream come true!") + .create(); + List releases = repository.getReleases(); + assertEquals(1, releases.size()); + GHRelease release = releases.get(0); + assertEquals("Test Release", release.getName()); + return release; + } + + private void commitTestFile(GHMyself myself, File repoDir, Git origin) throws IOException, GitAPIException { + File dummyFile = createDummyFile(repoDir); + DirCache cache = origin.add().addFilepattern(dummyFile.getName()).call(); + origin.commit().setMessage("test commit").call(); + origin.push().setCredentialsProvider(getCredentialsProvider(myself)).call(); + } + + private UsernamePasswordCredentialsProvider getCredentialsProvider(GHMyself myself) throws IOException { + Properties props = new Properties(); + File homeDir = new File(System.getProperty("user.home")); + FileInputStream in = new FileInputStream(new File(homeDir, ".github")); + try { + props.load(in); + } finally { + IOUtils.closeQuietly(in); + } + return new UsernamePasswordCredentialsProvider(props.getProperty("login"), props.getProperty("password")); + } + + private void delete(File toDelete) { + if (toDelete.isDirectory()) { + for (File file : toDelete.listFiles()) { + delete(file); + } + } + toDelete.delete(); + } + + private File createDummyFile(File repoDir) throws IOException { + File file = new File(repoDir, "testFile-" + System.currentTimeMillis()); + PrintWriter writer = new PrintWriter(new FileWriter(file)); + try { + writer.println("test file"); + } finally { + writer.close(); + } + return file; + } +}