diff --git a/pom.xml b/pom.xml index 48d7ce7b8e..3ab470b664 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.47 + 1.48 GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -16,6 +16,7 @@ scm:git:git@github.com/kohsuke/${project.artifactId}.git scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git http://${project.artifactId}.kohsuke.org/ + github-api-1.48 diff --git a/src/main/java/org/kohsuke/github/GHContent.java b/src/main/java/org/kohsuke/github/GHContent.java new file mode 100644 index 0000000000..adc1285ed9 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHContent.java @@ -0,0 +1,171 @@ +package org.kohsuke.github; + +import java.io.IOException; + +import javax.xml.bind.DatatypeConverter; + +/** + * A Content of a repository. + * + * @author Alexandre COLLIGNON + */ +public final class GHContent { + private GHRepository owner; + + private String type; + private String encoding; + private long size; + private String sha; + private String name; + private String path; + private String content; + private String url; // this is the API url + private String git_url; // this is the Blob url + private String html_url; // this is the UI + + public GHRepository getOwner() { + return owner; + } + + public String getType() { + return type; + } + + public String getEncoding() { + return encoding; + } + + public long getSize() { + return size; + } + + public String getSha() { + return sha; + } + + public String getName() { + return name; + } + + public String getPath() { + return path; + } + + /** + * Retrieve the decoded content that is stored at this location. + * + * Due to the nature of GitHub's API, you're not guaranteed that + * the content will already be populated, so this may trigger + * network activity, and can throw an IOException. + **/ + public String getContent() throws IOException { + return new String(DatatypeConverter.parseBase64Binary(getEncodedContent())); + } + + /** + * Retrieve the raw content that is stored at this location. + * + * Due to the nature of GitHub's API, you're not guaranteed that + * the content will already be populated, so this may trigger + * network activity, and can throw an IOException. + **/ + public String getEncodedContent() throws IOException { + if (content != null) + return content; + + GHContent retrievedContent = owner.getFileContent(path); + + this.size = retrievedContent.size; + this.sha = retrievedContent.sha; + this.content = retrievedContent.content; + this.url = retrievedContent.url; + this.git_url = retrievedContent.git_url; + this.html_url = retrievedContent.html_url; + + return content; + } + + public String getUrl() { + return url; + } + + public String getGitUrl() { + return git_url; + } + + public String getHtmlUrl() { + return html_url; + } + + public boolean isFile() { + return "file".equals(type); + } + + public boolean isDirectory() { + return "dir".equals(type); + } + + public GHContentUpdateResponse update(String newContent, String commitMessage) throws IOException { + return update(newContent, commitMessage, null); + } + + public GHContentUpdateResponse update(String newContent, String commitMessage, String branch) throws IOException { + String encodedContent = DatatypeConverter.printBase64Binary(newContent.getBytes()); + + Requester requester = new Requester(owner.root) + .with("path", path) + .with("message", commitMessage) + .with("sha", sha) + .with("content", encodedContent) + .method("PUT"); + + if (branch != null) { + requester.with("branch", branch); + } + + GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class); + + response.getContent().wrap(owner); + response.getCommit().wrapUp(owner); + + this.content = encodedContent; + return response; + } + + public GHContentUpdateResponse delete(String message) throws IOException { + return delete(message, null); + } + + public GHContentUpdateResponse delete(String commitMessage, String branch) throws IOException { + Requester requester = new Requester(owner.root) + .with("path", path) + .with("message", commitMessage) + .with("sha", sha) + .method("DELETE"); + + if (branch != null) { + requester.with("branch", branch); + } + + GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class); + + response.getCommit().wrapUp(owner); + return response; + } + + private String getApiRoute() { + return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/contents/" + path; + } + + GHContent wrap(GHRepository owner) { + this.owner = owner; + return this; + } + + public static GHContent[] wrap(GHContent[] contents, GHRepository repository) { + for (GHContent unwrappedContent : contents) { + unwrappedContent.wrap(repository); + } + return contents; + } +} diff --git a/src/main/java/org/kohsuke/github/GHContentUpdateResponse.java b/src/main/java/org/kohsuke/github/GHContentUpdateResponse.java new file mode 100644 index 0000000000..faa0545ad6 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHContentUpdateResponse.java @@ -0,0 +1,18 @@ +package org.kohsuke.github; + +/** + * The response that is returned when updating + * repository content. +**/ +public final class GHContentUpdateResponse { + private GHContent content; + private GHCommit commit; + + public GHContent getContent() { + return content; + } + + public GHCommit getCommit() { + return commit; + } +} diff --git a/src/main/java/org/kohsuke/github/GHKey.java b/src/main/java/org/kohsuke/github/GHKey.java index e9692453d4..91632cf175 100644 --- a/src/main/java/org/kohsuke/github/GHKey.java +++ b/src/main/java/org/kohsuke/github/GHKey.java @@ -10,9 +10,9 @@ public class GHKey { /*package almost final*/ GitHub root; - private String url, key, title; - private boolean verified; - private int id; + protected String url, key, title; + protected boolean verified; + protected int id; public int getId() { return id; diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java index 7a612007ac..70e732ac9e 100644 --- a/src/main/java/org/kohsuke/github/GHMyself.java +++ b/src/main/java/org/kohsuke/github/GHMyself.java @@ -34,6 +34,9 @@ public List getEmails() throws IOException { /** * Returns the read-only list of all the pulic keys of the current user. * + * NOTE: When using OAuth authenticaiton, the READ/WRITE User scope is + * required by the GitHub APIs, otherwise you will get a 404 NOT FOUND. + * * @return * Always non-null. */ @@ -41,6 +44,21 @@ public List getPublicKeys() throws IOException { return Collections.unmodifiableList(Arrays.asList(root.retrieve().to("/user/keys", GHKey[].class))); } + /** + * Returns the read-only list of all the pulic verified keys of the current user. + * + * Differently from the getPublicKeys() method, the retrieval of the user's + * verified public keys does not require any READ/WRITE OAuth Scope to the + * user's profile. + * + * @return + * Always non-null. + */ + public List getPublicVerifiedKeys() throws IOException { + return Collections.unmodifiableList(Arrays.asList(root.retrieve().to( + "/users/" + getLogin() + "/keys", GHVerifiedKey[].class))); + } + /** * Gets the organization that this user belongs to. */ diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index f1a3100a46..c1146f8e4b 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import javax.xml.bind.DatatypeConverter; import static java.util.Arrays.*; @@ -733,7 +734,66 @@ public GHMilestone getMilestone(int number) throws IOException { } return m; } - + + public GHContent getFileContent(String path) throws IOException { + return getFileContent(path, null); + } + + public GHContent getFileContent(String path, String ref) throws IOException { + Requester requester = root.retrieve(); + String target = String.format("/repos/%s/%s/contents/%s", owner.login, name, path); + + if (ref != null) + target = target + "?ref=" + ref; + + return requester.to(target, GHContent.class).wrap(this); + } + + public List getDirectoryContent(String path) throws IOException { + return getDirectoryContent(path, null); + } + + public List getDirectoryContent(String path, String ref) throws IOException { + Requester requester = root.retrieve(); + String target = String.format("/repos/%s/%s/contents/%s", owner.login, name, path); + + if (ref != null) + target = target + "?ref=" + ref; + + GHContent[] files = requester.to(target, GHContent[].class); + + GHContent.wrap(files, this); + + return Arrays.asList(files); + } + + public GHContent getReadme() throws Exception { + return getFileContent("readme"); + } + + public GHContentUpdateResponse createContent(String content, String commitMessage, String path) throws IOException { + return createContent(content, commitMessage, path, null); + } + + public GHContentUpdateResponse createContent(String content, String commitMessage, String path, String branch) throws IOException { + Requester requester = new Requester(root) + .with("path", path) + .with("message", commitMessage) + .with("content", DatatypeConverter.printBase64Binary(content.getBytes())) + .method("PUT"); + + if (branch != null) { + requester.with("branch", branch); + } + + GHContentUpdateResponse response = requester.to(getApiTailUrl("contents/" + path), GHContentUpdateResponse.class); + + response.getContent().wrap(this); + response.getCommit().wrapUp(this); + + return response; + } + public GHMilestone createMilestone(String title, String description) throws IOException { return new Requester(root) .with("title", title).with("description", description).method("POST").to(getApiTailUrl("milestones"), GHMilestone.class).wrap(this); diff --git a/src/main/java/org/kohsuke/github/GHVerifiedKey.java b/src/main/java/org/kohsuke/github/GHVerifiedKey.java new file mode 100644 index 0000000000..d81597d748 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHVerifiedKey.java @@ -0,0 +1,13 @@ +package org.kohsuke.github; + +public class GHVerifiedKey extends GHKey { + + public GHVerifiedKey() { + this.verified = true; + } + + @Override + public String getTitle() { + return (title == null ? "key-" + id : title); + } +} diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java index 64908e7a87..0b8177657c 100644 --- a/src/test/java/org/kohsuke/AppTest.java +++ b/src/test/java/org/kohsuke/AppTest.java @@ -46,6 +46,9 @@ public void setUp() throws Exception { } public void testRepoCRUD() throws Exception { + GHRepository existing = gitHub.getMyself().getRepository("github-api-test"); + if (existing!=null) + existing.delete(); GHRepository r = gitHub.createRepository("github-api-test", "a test repository", "http://github-api.kohsuke.org/", true); r.enableIssueTracker(false); r.enableDownloads(false); diff --git a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java new file mode 100644 index 0000000000..d375f85403 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java @@ -0,0 +1,67 @@ +package org.kohsuke.github; + +import junit.framework.TestCase; + +import java.util.List; +import java.util.UUID; + +/** + * Integration test for {@link GHContent}. + */ +public class GHContentIntegrationTest extends TestCase { + + private GitHub gitHub; + private GHRepository repo; + private String createdFilename; + + @Override + public void setUp() throws Exception { + super.setUp(); + + gitHub = GitHub.connect(); + repo = gitHub.getRepository("acollign/github-api-test").fork(); + createdFilename = UUID.randomUUID().toString(); + } + + public void testGetFileContent() throws Exception { + GHContent content = repo.getFileContent("ghcontent-ro/a-file-with-content"); + + assertTrue(content.isFile()); + assertEquals("thanks for reading me\n", content.getContent()); + } + + public void testGetEmptyFileContent() throws Exception { + GHContent content = repo.getFileContent("ghcontent-ro/an-empty-file"); + + assertTrue(content.isFile()); + assertEquals("", content.getContent()); + } + + public void testGetDirectoryContent() throws Exception { + List entries = repo.getDirectoryContent("ghcontent-ro/a-dir-with-3-entries"); + + assertTrue(entries.size() == 3); + } + + public void testCRUDContent() throws Exception { + GHContentUpdateResponse created = repo.createContent("this is an awesome file I created\n", "Creating a file for integration tests.", createdFilename); + GHContent createdContent = created.getContent(); + + assertNotNull(created.getCommit()); + assertNotNull(created.getContent()); + assertNotNull(createdContent.getContent()); + assertEquals("this is an awesome file I created\n", createdContent.getContent()); + + GHContentUpdateResponse updatedContentResponse = createdContent.update("this is some new content\n", "Updated file for integration tests."); + GHContent updatedContent = updatedContentResponse.getContent(); + + assertNotNull(updatedContentResponse.getCommit()); + assertNotNull(updatedContentResponse.getContent()); + assertEquals("this is some new content\n", updatedContent.getContent()); + + GHContentUpdateResponse deleteResponse = updatedContent.delete("Enough of this foolishness!"); + + assertNotNull(deleteResponse.getCommit()); + assertNull(deleteResponse.getContent()); + } +}