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;
+ }
+}