├── .gitignore ├── test └── tentacles │ ├── gists_test.clj │ ├── search_test.clj │ └── core_test.clj ├── project.clj ├── src └── tentacles │ ├── oauth.clj │ ├── events.clj │ ├── users.clj │ ├── pulls.clj │ ├── gists.clj │ ├── data.clj │ ├── core.clj │ ├── orgs.clj │ ├── search.clj │ ├── issues.clj │ └── repos.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-deps-sum 6 | testinfo.clj 7 | .lein* 8 | target/ 9 | docs/ 10 | .nrepl-port 11 | .idea/* 12 | *.iml -------------------------------------------------------------------------------- /test/tentacles/gists_test.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.gists-test 2 | (:use clojure.test) 3 | (:require [tentacles.gists :as gists])) 4 | 5 | (def gist {:files {:file1 {:filename "file1" :content "content1" :type "text/plain"} :file2 {:filename "file2" :content "content2"}}}) 6 | 7 | (deftest files-are-parsed 8 | (let [files (gists/file-contents gist)] 9 | (is (= (count files) 2)) 10 | (is (= (:file1 files) "content1")))) 11 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject tentacles "0.5.2-SNAPSHOT" 2 | :description "A library for working with the Github API." 3 | :url "https://github.com/Raynes/tentacles" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [clj-http "3.1.0"] 8 | [cheshire "5.6.3"] 9 | [com.cemerick/url "0.1.1"] 10 | [org.clojure/data.codec "0.1.0"] 11 | [environ "1.1.0"]]) 12 | -------------------------------------------------------------------------------- /test/tentacles/search_test.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.search-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [tentacles.search :refer [search-term]])) 4 | 5 | (deftest query-should-not-contains-text-keyword 6 | (is (= "foo bar language:clojure language:scala" 7 | (search-term ["foo" "bar"] {:language ["clojure" "scala"]})))) 8 | 9 | (deftest qualifier-with-one-element-seq-should-equals-qualifier-with-string-element 10 | (is (= (search-term ["foo"] {:language ["clojure"]}) 11 | (search-term "foo" {:language "clojure"})))) 12 | 13 | (deftest query-should-not-contains-nil-qualifier 14 | (is (= "foo" 15 | (search-term "foo" {:language nil})))) 16 | -------------------------------------------------------------------------------- /src/tentacles/oauth.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.oauth 2 | (:use [tentacles.core :only [api-call no-content?]])) 3 | 4 | (defn authorizations 5 | "List your authorizations." 6 | [options] 7 | (api-call :get "authorizations" nil options)) 8 | 9 | (defn specific-auth 10 | "Get a specific authorization." 11 | [id & [options]] 12 | (api-call :get "authorizations/%s" [id] options)) 13 | 14 | (defn delete-auth 15 | "Delete an authorization." 16 | [id & [options]] 17 | (no-content? (api-call :delete "authorizations/%s" [id] options))) 18 | 19 | (defn create-auth 20 | "Create a new authorization." 21 | [options] 22 | (api-call :post "authorizations" nil options)) 23 | 24 | (defn valid-auth? 25 | "Returns auth data if authorization is still valid, false otherwise. 26 | OAuth applications can use this special API method for checking 27 | OAuth token validity without running afoul of normal rate limits for 28 | failed login attempts. Authentication works differently with this 29 | particular endpoint. You must use Basic Authentication when 30 | accessing it, where the username is the OAuth application client_id 31 | and the password is its client_secret." 32 | [client-id access-token & [options]] 33 | (let [result (api-call :get "applications/%s/tokens/%s" [client-id access-token] options)] 34 | (if (= (:status result) 404) 35 | false 36 | result))) 37 | -------------------------------------------------------------------------------- /src/tentacles/events.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.events 2 | "Implements the Github Events API: http://developer.github.com/v3/events/" 3 | (:use [tentacles.core :only [api-call]])) 4 | 5 | (defn events 6 | "List public events." 7 | [& [options]] 8 | (api-call :get "events" nil options)) 9 | 10 | (defn repo-events 11 | "List repository events." 12 | [user repo & [options]] 13 | (api-call :get "repos/%s/%s/events" [user repo] options)) 14 | 15 | (defn issue-events 16 | "List issue events for a repository." 17 | [user repo & [options]] 18 | (api-call :get "repos/%s/%s/issues/events" [user repo] options)) 19 | 20 | (defn network-events 21 | "List events for a network of repositories." 22 | [user repo & [options]] 23 | (api-call :get "networks/%s/%s/events" [user repo] options)) 24 | 25 | (defn user-events 26 | "List events that a user has received. If authenticated, you'll see 27 | private events, otherwise only public." 28 | [user & [options]] 29 | (api-call :get "users/%s/received_events" [user] options)) 30 | 31 | (defn performed-events 32 | "List events performed by a user. If you're authenticated, you'll see 33 | private events, otherwise you'll only see public events." 34 | [user & [options]] 35 | (api-call :get "users/%s/events" [user] options)) 36 | 37 | ;; Even though this requires authentication, you still need to pass the 38 | ;; username in the URL. I can work around this, but I don't feel like it 39 | ;; right now. 40 | (defn org-events 41 | "List an organization's events." 42 | [user org options] 43 | (api-call :get "users/%s/events/orgs/%s" [user org] options)) -------------------------------------------------------------------------------- /test/tentacles/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.core-test 2 | (:use clojure.test) 3 | (:require [tentacles.core :as core])) 4 | 5 | (deftest patch-requests-encode-json-body 6 | (let [request (core/make-request :patch "foo" nil {:bar "baz"})] 7 | (is (= (:body request) "{\"bar\":\"baz\"}")))) 8 | 9 | (deftest request-contains-user-agent 10 | (let [request (core/make-request :get "test" nil {:user-agent "Mozilla"})] 11 | (do 12 | (is (empty? (:query-params request))) 13 | (is (contains? (:headers request) "User-Agent")) 14 | (is (= (get (:headers request) "User-Agent") "Mozilla"))))) 15 | 16 | (deftest request-contains-user-agent-from-defaults 17 | (core/with-defaults {:user-agent "Mozilla"} 18 | (let [request (core/make-request :get "test" nil {})] 19 | (do 20 | (is (empty? (:query-params request))) 21 | (is (contains? (:headers request) "User-Agent")) 22 | (is (= (get (:headers request) "User-Agent") "Mozilla")))))) 23 | 24 | (deftest adhoc-options-override-defaults 25 | (core/with-defaults {:user-agent "default"} 26 | (let [request (core/make-request :get "test" nil {:user-agent "adhoc"})] 27 | (do 28 | (is (empty? (:query-params request))) 29 | (is (contains? (:headers request) "User-Agent")) 30 | (is (= (get (:headers request) "User-Agent") "adhoc")))))) 31 | 32 | (deftest hitting-rate-limit-is-propagated 33 | (is (= (:status (core/safe-parse {:status 403})) 34 | 403))) 35 | 36 | (deftest rate-limit-details-are-propagated 37 | (is (= 60 (:call-limit (core/api-meta 38 | (core/safe-parse {:status 200 :headers {"x-ratelimit-limit" "60" 39 | "content-type" ""}})))))) 40 | 41 | (deftest poll-limit-details-are-propagated 42 | (is (= 61 (:poll-interval (core/api-meta 43 | (core/safe-parse {:status 200 44 | :headers {"x-poll-interval" "61" 45 | "content-type" ""}})))))) 46 | -------------------------------------------------------------------------------- /src/tentacles/users.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.users 2 | "Implement the Github Users API: http://developer.github.com/v3/users/" 3 | (:refer-clojure :exclude [keys]) 4 | (:use [tentacles.core :only [api-call no-content?]])) 5 | 6 | (defn users 7 | "Get info about all users." 8 | [& [options]] 9 | (api-call :get "users" nil options)) 10 | 11 | (defn user 12 | "Get info about a user." 13 | [user & [options]] 14 | (api-call :get "users/%s" [user] options)) 15 | 16 | (defn me 17 | "Get info about the currently authenticated user." 18 | [& [options]] 19 | (api-call :get "user" nil options)) 20 | 21 | (defn edit-user 22 | "Edit the currently authenticated user. 23 | Options are: 24 | name -- User's name. 25 | email -- User's email. 26 | blog -- Link to user's blog. 27 | location -- User's location. 28 | hireable -- Looking for a job? 29 | bio -- User's biography." 30 | [options] 31 | (api-call :patch "user" nil options)) 32 | 33 | (defn emails 34 | "List the authenticated user's emails." 35 | [options] 36 | (api-call :get "user/emails" nil options)) 37 | 38 | (defn add-emails 39 | "Add email address(es) to the authenticated user. emails is either 40 | a string or a sequence of emails addresses." 41 | [emails options] 42 | (api-call :post "user/emails" nil (assoc options :raw emails))) 43 | 44 | (defn delete-emails 45 | "Delete email address(es) from the authenticated user. Emails is either 46 | a string or a sequence of email addresses." 47 | [emails options] 48 | (no-content? (api-call :delete "user/emails" nil (assoc options :raw emails)))) 49 | 50 | (defn followers 51 | "List a user's followers." 52 | [user & [options]] 53 | (api-call :get "users/%s/followers" [user] options)) 54 | 55 | (defn my-followers 56 | "List the authenticated user's followers." 57 | [options] 58 | (api-call :get "user/followers" nil options)) 59 | 60 | (defn following 61 | "List the users a user is following." 62 | [user & [options]] 63 | (api-call :get "users/%s/following" [user] options)) 64 | 65 | (defn my-following 66 | "List the users the authenticated user is following." 67 | [options] 68 | (api-call :get "user/following" nil options)) 69 | 70 | (defn following? 71 | "Check if the authenticated user is following another user." 72 | [user options] 73 | (no-content? (api-call :get "user/following/%s" [user] options))) 74 | 75 | (defn follow 76 | "Follow a user." 77 | [user options] 78 | (no-content? (api-call :put "user/following/%s" [user] options))) 79 | 80 | (defn unfollow 81 | "Unfollow a user." 82 | [user options] 83 | (no-content? (api-call :delete "user/following/%s" [user] options))) 84 | 85 | (defn user-keys 86 | "List the user's public keys." 87 | [user & [options]] 88 | (api-call :get "users/%s/keys" [user] options)) 89 | 90 | (defn keys 91 | "List the authenticated user's public keys." 92 | [options] 93 | (api-call :get "user/keys" nil options)) 94 | 95 | (defn specific-key 96 | "Get a specific key from the authenticated user." 97 | [id options] 98 | (api-call :get "user/keys/%s" [id] options)) 99 | 100 | (defn create-key 101 | "Create a new public key." 102 | [title key options] 103 | (api-call :post "user/keys" nil (assoc options :title title :key key))) 104 | 105 | (defn delete-key 106 | "Delete a public key." 107 | [id options] 108 | (no-content? (api-call :delete "user/keys/%s" [id] options))) 109 | 110 | (defn my-teams 111 | "List the currently authenticated user's teams across all organizations" 112 | [& [options]] 113 | (api-call :get "user/teams" nil options)) 114 | 115 | (defn repos 116 | "All repositories for a user." 117 | [user & [options]] 118 | (api-call :get "users/%s/repos" [user] options)) 119 | -------------------------------------------------------------------------------- /src/tentacles/pulls.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.pulls 2 | "Implement the Github Pull Requests API: http://developer.github.com/v3/pulls/" 3 | (:refer-clojure :exclude [merge]) 4 | (:use [tentacles.core :only [api-call no-content?]])) 5 | 6 | (defn pulls 7 | "List pull requests on a repo. 8 | Options are: 9 | state -- open (default), closed." 10 | [user repo & [options]] 11 | (api-call :get "repos/%s/%s/pulls" [user repo] options)) 12 | 13 | (defn specific-pull 14 | "Get a specific pull request." 15 | [user repo id & [options]] 16 | (api-call :get "repos/%s/%s/pulls/%s" [user repo id] options)) 17 | 18 | (defn create-pull 19 | "Create a new pull request. If from is a number, it is considered 20 | to be an issue number on the repository in question. If this is used, 21 | the pull request will be created from the existing issue. If it is a 22 | string it is considered to be a title. base is the branch or ref that 23 | you want your changes pulled into, and head is the branch or ref where 24 | your changes are implemented. 25 | Options are: 26 | body -- The body of the pull request text. Only applies when not 27 | creating a pull request from an issue." 28 | ([user repo from base head options] 29 | (api-call :post "repos/%s/%s/pulls" [user repo] 30 | (let [base-opts (assoc options 31 | :base base 32 | :head head)] 33 | (if (number? from) 34 | (assoc base-opts :issue from) 35 | (assoc base-opts :title from)))))) 36 | 37 | (defn edit-pull 38 | "Edit a pull request. 39 | Options are: 40 | title -- a new title. 41 | body -- a new body. 42 | state -- open or closed." 43 | [user repo id options] 44 | (api-call :patch "repos/%s/%s/pulls/%s" [user repo id] options)) 45 | 46 | (defn commits 47 | "List the commits on a pull request." 48 | [user repo id & [options]] 49 | (api-call :get "repos/%s/%s/pulls/%s/commits" [user repo id] options)) 50 | 51 | (defn files 52 | "List the files on a pull request." 53 | [user repo id & [options]] 54 | (api-call :get "repos/%s/%s/pulls/%s/files" [user repo id] options)) 55 | 56 | (defn merged? 57 | "Check if a pull request has been merged." 58 | [user repo id & [options]] 59 | (no-content? (api-call :get "repos/%s/%s/pulls/%s/merge" [user repo id] options))) 60 | 61 | (defn merge 62 | "Merge a pull request. 63 | Options are: 64 | commit-message -- A commit message for the merge commit." 65 | [user repo id options] 66 | (api-call :put "repos/%s/%s/pulls/%s/merge" [user repo id] options)) 67 | 68 | ;; ## Pull Request Comment API 69 | 70 | (defn repo-comments 71 | "List pull request comments in a repository." 72 | [user repo & [options]] 73 | (api-call :get "repos/%s/%s/pulls/comments" [user repo] options)) 74 | 75 | (defn comments 76 | "List comments on a pull request." 77 | [user repo id & [options]] 78 | (api-call :get "repos/%s/%s/pulls/%s/comments" [user repo id] options)) 79 | 80 | (defn specific-comment 81 | "Get a specific comment on a pull request." 82 | [user repo id & [options]] 83 | (api-call :get "repos/%s/%s/pulls/comments/%s" [user repo id] options)) 84 | 85 | ;; You're supposed to be able to reply to comments as well, but that doesn't seem 86 | ;; to actually work. Commenting tha 87 | (defn create-comment 88 | "Create a comment on a pull request." 89 | [user repo id sha path position body options] 90 | (api-call :post "repos/%s/%s/pulls/%s/comments" [user repo id] 91 | (assoc options 92 | :commit-id sha 93 | :path path 94 | :position position 95 | :body body))) 96 | 97 | (defn edit-comment 98 | "Edit a comment on a pull request." 99 | [user repo id body options] 100 | (api-call :patch "repos/%s/%s/pulls/comments/%s" [user repo id] 101 | (assoc options :body body))) 102 | 103 | (defn delete-comment 104 | "Delete a comment on a pull request." 105 | [user repo id options] 106 | (no-content? (api-call :delete "repos/%s/%s/pulls/comments/%s" [user repo id] options))) 107 | -------------------------------------------------------------------------------- /src/tentacles/gists.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.gists 2 | "Implements the Github Gists API: http://developer.github.com/v3/gists/" 3 | (:use [tentacles.core :only [api-call no-content?]])) 4 | 5 | ;; ## Primary gist API 6 | 7 | (defn user-gists 8 | "List a user's gists." 9 | [user & [options]] 10 | (api-call :get "users/%s/gists" [user] options)) 11 | 12 | (defn gists 13 | "If authenticated, list the authenticated user's gists. Otherwise, 14 | return all public gists." 15 | [& [options]] 16 | (api-call :get "gists" nil options)) 17 | 18 | (defn public-gists 19 | "List all public gists." 20 | [] 21 | (api-call :get "gists/public" nil nil)) 22 | 23 | (defn starred-gists 24 | "List the authenticated user's starred gists." 25 | [options] 26 | (api-call :get "gists/starred" nil options)) 27 | 28 | (defn specific-gist 29 | "Get a specific gist." 30 | [id & [options]] 31 | (api-call :get "gists/%s" [id] options)) 32 | 33 | ;; For whatever insane reason, Github expects gist files to be passed 34 | ;; as a JSON hash of filename -> hash of content -> contents rather than 35 | ;; just filename -> contents. I'm not going to be a dick and require that 36 | ;; users of this library pass maps like that. 37 | ;; 38 | ;; It *does* make sense in edit-gist, however, since we can selectively update 39 | ;; a file's name and/or content, or both. I imagine that Github chose to require 40 | ;; a subhash with a content key in the creation api end-point for consistency. 41 | ;; In our case, I think I'd rather have a sensible gist creation function. 42 | (defn- file-map [options files] 43 | (assoc options 44 | :files (into {} (for [[k v] files] [k {:content v}])))) 45 | 46 | (defn file-contents 47 | "Extract a file->content map from a gist" 48 | [gist] 49 | (if-let [files (:files gist)] 50 | (zipmap (keys files) (map (comp :content second) files)))) 51 | 52 | (defn create-gist 53 | "Create a gist. files is a map of filenames to contents. 54 | Options are: 55 | description -- A string description of the gist. 56 | public -- true (default) or false; whether or not the gist is public." 57 | [files & [options]] 58 | (api-call :post "gists" nil 59 | (assoc (file-map options files) 60 | :public (:public options true)))) 61 | 62 | ;; It makes sense to require the user to pass :files the way Github expects it 63 | ;; here: as a map of filenames to maps of :contents and/or :filename. It makes 64 | ;; sense because users can selectively update only certain parts of a gist. A 65 | ;; map is a clean way to express this update. 66 | (defn edit-gist 67 | "Edit a gist. 68 | Options are: 69 | description -- A string to update the description to. 70 | files -- A map of filenames to maps. These submaps may 71 | contain either of the following, or both: a 72 | :contents key that will replace the gist's 73 | contents, and a :filename key that will replace 74 | the name of the file. If one of the file keys in 75 | the map is associated with 'nil', it'll be deleted." 76 | [id & [options]] 77 | (api-call :patch "gists/%s" [id] options)) 78 | 79 | (defn star-gist 80 | "Star a gist." 81 | [id & [options]] 82 | (no-content? (api-call :put "gists/%s/star" [id] options))) 83 | 84 | (defn unstar-gist 85 | "Unstar a gist." 86 | [id & [options]] 87 | (no-content? (api-call :delete "gists/%s/star" [id] options))) 88 | 89 | ;; Github sends 404 which clj-http throws an exception for if a gist 90 | ;; is not starred. I'd rather get back true or false. 91 | (defn starred? 92 | "Check if a gist is starred." 93 | [id & [options]] 94 | (no-content? (api-call :get "gists/%s/star" [id] options))) 95 | 96 | (defn fork-gist 97 | "Fork a gist." 98 | [id & [options]] 99 | (api-call :post "gists/%s/forks" [id] options)) 100 | 101 | (defn delete-gist 102 | "Delete a gist." 103 | [id & [options]] 104 | (no-content? (api-call :delete "gists/%s" [id] options))) 105 | 106 | ;; ## Gist Comments API 107 | 108 | (defn comments 109 | "List comments for a gist." 110 | [id & [options]] 111 | (api-call :get "gists/%s/comments" [id] options)) 112 | 113 | (defn specific-comment 114 | "Get a specific comment." 115 | [comment-id & [options]] 116 | (api-call :get "gists/comments/%s" [comment-id] options)) 117 | 118 | (defn create-comment 119 | "Create a comment." 120 | [id body options] 121 | (api-call :post "gists/%s/comments" [id] (assoc options :body body))) 122 | 123 | (defn edit-comment 124 | "Edit a comment." 125 | [comment-id body options] 126 | (api-call :patch "gists/comments/%s" [comment-id] (assoc options :body body))) 127 | 128 | (defn delete-comment 129 | "Delete a comment." 130 | [comment-id options] 131 | (no-content? (api-call :delete "gists/comments/%s" [comment-id] options))) 132 | -------------------------------------------------------------------------------- /src/tentacles/data.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.data 2 | "Implements the Git Data API: http://developer.github.com/v3/git/blobs/" 3 | (:use [tentacles.core :only [api-call]])) 4 | 5 | ;; ## Blobs 6 | 7 | (defn blob 8 | "Get a blob." 9 | [user repo sha & [options]] 10 | (api-call :get "repos/%s/%s/git/blobs/%s" [user repo sha] options)) 11 | 12 | (defn create-blob 13 | "Create a blob." 14 | [user repo content encoding options] 15 | (api-call :post "repos/%s/%s/git/blobs" [user repo] 16 | (assoc options 17 | :content content 18 | :encoding encoding))) 19 | 20 | ;; ## Commits 21 | 22 | (defn commit 23 | "Get a commit." 24 | [user repo sha & [options]] 25 | (api-call :get "repos/%s/%s/git/commits/%s" [user repo sha] options)) 26 | 27 | (defn create-commit 28 | "Create a commit. 29 | Options are: 30 | parents -- A sequence of SHAs of the commits that were 31 | the parents of this commit. If omitted, the 32 | commit will be written as a root commit. 33 | author -- A map of the following (string keys): 34 | \"name\" -- Name of the author of the commit. 35 | \"email\" -- Email of the author of the commit. 36 | \"date\" -- Timestamp when this commit was authored. 37 | committer -- A map of the following (string keys): 38 | \"name\" -- Name of the committer of the commit. 39 | \"email\" -- Email of the committer of the commit. 40 | \"date\" -- Timestamp when this commit was committed. 41 | If the committer section is omitted, then it will be filled in with author 42 | data. If that is omitted, information will be obtained using the 43 | authenticated user's information and the current date." 44 | [user repo message tree options] 45 | (api-call :post "repos/%s/%s/git/commits" [user repo] 46 | (assoc options 47 | :message message 48 | :tree tree))) 49 | 50 | ;; ## References 51 | 52 | (defn reference 53 | "Get a reference." 54 | [user repo ref & [options]] 55 | (api-call :get "repos/%s/%s/git/refs/%s" [user repo ref] options)) 56 | 57 | (defn references 58 | "Get all references." 59 | [user repo & [options]] 60 | (api-call :get "repos/%s/%s/git/refs" [user repo] options)) 61 | 62 | (defn create-reference 63 | "Create a new reference." 64 | [user repo ref sha options] 65 | (api-call :post "repos/%s/%s/git/refs" [user repo] 66 | (assoc options 67 | :ref ref 68 | :sha sha))) 69 | 70 | (defn edit-reference 71 | "Edit a reference. 72 | Options are: 73 | force -- true or false (default); whether to force the update or make 74 | sure this is a fast-forward update." 75 | [user repo ref sha options] 76 | (api-call :post "repos/%s/%s/git/refs/%s" [user repo ref] 77 | (assoc options :sha sha))) 78 | 79 | 80 | ;; ## Tags 81 | 82 | (defn tag 83 | "Get a tag." 84 | [user repo sha & [options]] 85 | (api-call :get "repos/%s/%s/git/tags/%s" [user repo sha] options)) 86 | 87 | (defn tags 88 | "Get several tags." 89 | [user repo & [options]] 90 | (api-call :get "repos/%s/%s/git/refs/tags" [user repo] options)) 91 | 92 | ;; The API documentation is unclear about which parts of this API call 93 | ;; are optional. 94 | (defn create-tag 95 | "Create a tag object. Note that this does not create the reference 96 | that makes a tag in Git. If you want to create an annotated tag, you 97 | have to do this call to create the tag object and then create the 98 | `refs/tags/[tag]` reference. If you want to create a lightweight tag, 99 | you simply need to create the reference and this call would be 100 | unnecessary. 101 | Options are: 102 | tagger -- A map (string keys) containing the following: 103 | \"name\" -- Name of the author of this tag. 104 | \"email\" -- Email of the author of this tag. 105 | \"date\" -- Timestamp when this object was tagged." 106 | [user repo tag message object type options] 107 | (api-call :post "repos/%s/%s/git/tags" [user repo] 108 | (assoc options 109 | :tag tag 110 | :message message 111 | :object object 112 | :type type))) 113 | 114 | ;; ## Trees 115 | 116 | (defn tree 117 | "Get a tree. 118 | Options are: 119 | recursive -- true or false; get a tree recursively?" 120 | [user repo sha & [options]] 121 | (api-call :get "repos/%s/%s/git/trees/%s" [user repo sha] options)) 122 | 123 | (defn create-tree 124 | "Create a tree. 'tree' is a map of the following (string keys): 125 | path -- The file referenced in the tree. 126 | mode -- The file mode; one of 100644 for file, 100755 for executable, 127 | 040000 for subdirectory, 160000 for submodule, or 120000 for 128 | a blob that specifies the path of a symlink. 129 | type -- blog, tree, or commit. 130 | sha -- SHA of the object in the tree. 131 | content -- Content that you want this file to have. 132 | Options are: 133 | base-tree -- SHA of the tree you want to update (if applicable)." 134 | [user repo tree options] 135 | (api-call :post "repos/%s/%s/git/trees" [user repo] 136 | (assoc options :tree tree))) 137 | 138 | -------------------------------------------------------------------------------- /src/tentacles/core.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.core 2 | (:require [clj-http.client :as http] 3 | [cheshire.core :as json] 4 | [clojure.string :as str] 5 | [cemerick.url :as url])) 6 | 7 | (def ^:dynamic url "https://api.github.com/") 8 | (def ^:dynamic defaults {}) 9 | 10 | (defn query-map 11 | "Turn keywords into strings, and replace hyphens with underscores." 12 | [entries] 13 | (into {} 14 | (for [[k v] entries] 15 | [(.replace (name k) "-" "_") v]))) 16 | 17 | (defn parse-json 18 | "Same as json/parse-string but handles nil gracefully." 19 | [s] (when s (json/parse-string s true))) 20 | 21 | (defn parse-link [link] 22 | (let [[_ url] (re-find #"<(.*)>" link) 23 | [_ rel] (re-find #"rel=\"(.*)\"" link)] 24 | [(keyword rel) url])) 25 | 26 | (defn parse-links 27 | "Takes the content of the link header from a github resp, returns a map of links" 28 | [link-body] 29 | (->> (str/split link-body #",") 30 | (map parse-link) 31 | (into {}))) 32 | 33 | (defn extract-useful-meta 34 | [h] 35 | (let [{:strs [etag last-modified x-ratelimit-limit x-ratelimit-remaining 36 | x-poll-interval]} 37 | h] 38 | {:etag etag :last-modified last-modified 39 | :call-limit (when x-ratelimit-limit (Long/parseLong x-ratelimit-limit)) 40 | :call-remaining (when x-ratelimit-remaining (Long/parseLong x-ratelimit-remaining)) 41 | :poll-interval (when x-poll-interval (Long/parseLong x-poll-interval))})) 42 | 43 | (defn api-meta 44 | [obj] 45 | (:api-meta (meta obj))) 46 | 47 | (defn safe-parse 48 | "Takes a response and checks for certain status codes. If 204, return nil. 49 | If 400, 401, 204, 422, 403, 404 or 500, return the original response with the body parsed 50 | as json. Otherwise, parse and return the body if json, or return the body if raw." 51 | [{:keys [headers status body] :as resp}] 52 | (cond 53 | (= 202 status) 54 | ::accepted 55 | (= 304 status) 56 | ::not-modified 57 | (#{400 401 204 422 403 404 500} status) 58 | (update-in resp [:body] parse-json) 59 | :else (let [links (parse-links (get headers "link" "")) 60 | content-type (get headers "content-type") 61 | metadata (extract-useful-meta headers)] 62 | (if-not (.contains content-type "raw") 63 | (let [parsed (parse-json body)] 64 | (if (map? parsed) 65 | (with-meta parsed {:links links :api-meta metadata}) 66 | (with-meta (map #(with-meta % metadata) parsed) 67 | {:links links :api-meta metadata}))) 68 | body)))) 69 | 70 | (defn update-req 71 | "Given a clj-http request, and a 'next' url string, merge the next url into the request" 72 | [req url] 73 | (let [url-map (url/url url)] 74 | (assoc-in req [:query-params] (:query url-map)))) 75 | 76 | (defn no-content? 77 | "Takes a response and returns true if it is a 204 response, false otherwise." 78 | [x] (= (:status x) 204)) 79 | 80 | (defn format-url 81 | "Creates a URL out of end-point and positional. Called URLEncoder/encode on 82 | the elements of positional and then formats them in." 83 | [end-point positional] 84 | (str url (apply format end-point (map url/url-encode positional)))) 85 | 86 | (defn make-request [method end-point positional query] 87 | (let [{:keys [auth throw-exceptions follow-redirects accept 88 | oauth-token etag if-modified-since user-agent 89 | otp] 90 | :or {follow-redirects true throw-exceptions false} 91 | :as query} (merge defaults query) 92 | req (merge-with merge 93 | {:url (format-url end-point positional) 94 | :basic-auth auth 95 | :throw-exceptions throw-exceptions 96 | :follow-redirects follow-redirects 97 | :method method} 98 | (when accept 99 | {:headers {"Accept" accept}}) 100 | (when oauth-token 101 | {:headers {"Authorization" (str "token " oauth-token)}}) 102 | (when etag 103 | {:headers {"if-None-Match" etag}}) 104 | (when user-agent 105 | {:headers {"User-Agent" user-agent}}) 106 | (when otp 107 | {:headers {"X-GitHub-OTP" otp}}) 108 | (when if-modified-since 109 | {:headers {"if-Modified-Since" if-modified-since}})) 110 | raw-query (:raw query) 111 | proper-query (query-map (dissoc query :auth :oauth-token :all-pages :accept :user-agent :otp)) 112 | req (if (#{:post :put :delete :patch} method) 113 | (assoc req :body (json/generate-string (or raw-query proper-query))) 114 | (assoc req :query-params proper-query))] 115 | req)) 116 | 117 | (defn api-call 118 | ([method end-point] (api-call method end-point nil nil)) 119 | ([method end-point positional] (api-call method end-point positional nil)) 120 | ([method end-point positional query] 121 | (let [query (or query {}) 122 | all-pages? (query :all-pages) 123 | req (make-request method end-point positional query) 124 | exec-request-one (fn exec-request-one [req] 125 | (safe-parse (http/request req))) 126 | exec-request (fn exec-request [req] 127 | (let [resp (exec-request-one req)] 128 | (if (and all-pages? (-> resp meta :links :next)) 129 | (let [new-req (update-req req (-> resp meta :links :next))] 130 | (lazy-cat resp (exec-request new-req))) 131 | resp)))] 132 | (exec-request req)))) 133 | 134 | (defn raw-api-call 135 | ([method end-point] (raw-api-call method end-point nil nil)) 136 | ([method end-point positional] (raw-api-call method end-point positional nil)) 137 | ([method end-point positional query] 138 | (let [query (or query {}) 139 | all-pages? (query :all-pages) 140 | req (make-request method end-point positional query)] 141 | (http/request req)))) 142 | 143 | (defn environ-auth 144 | "Lookup :gh-username and :gh-password in environ (~/.lein/profiles.clj or .lein-env) and return a string auth. 145 | Usage: (users/me {:auth (environ-auth)})" 146 | [env] 147 | (str (:gh-username env ) ":" (:gh-password env))) 148 | 149 | (defn rate-limit 150 | ([] (api-call :get "rate_limit")) 151 | ([opts] (api-call :get "rate_limit" nil opts))) 152 | 153 | (defmacro with-url [new-url & body] 154 | `(binding [url ~new-url] 155 | ~@body)) 156 | 157 | (defmacro with-defaults [options & body] 158 | `(binding [defaults ~options] 159 | ~@body)) 160 | -------------------------------------------------------------------------------- /src/tentacles/orgs.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.orgs 2 | "Implements the Github Orgs API: http://developer.github.com/v3/orgs/" 3 | (:use [tentacles.core :only [api-call no-content?]])) 4 | 5 | ;; ## Primary API 6 | 7 | (defn user-orgs 8 | "List the public organizations for a user." 9 | [user] 10 | (api-call :get "users/%s/orgs" [user] nil)) 11 | 12 | (defn orgs 13 | "List the public and private organizations for the currently 14 | authenticated user." 15 | [options] 16 | (api-call :get "user/orgs" nil options)) 17 | 18 | (defn specific-org 19 | "Get a specific organization." 20 | [org & [options]] 21 | (api-call :get "orgs/%s" [org] options)) 22 | 23 | (defn repos 24 | "All repositories in the organization" 25 | [org & [options]] 26 | (api-call :get "orgs/%s/repos" [org] options)) 27 | 28 | (defn edit-org 29 | "Edit an organization. 30 | Options are: 31 | billing-email -- Billing email address. 32 | company -- The name of the company the organization belongs to. 33 | email -- Publically visible email address. 34 | location -- Organization location. 35 | name -- Name of the organization." 36 | [org options] 37 | (api-call :patch "orgs/%s" [org] options)) 38 | 39 | ;; ## Org Members API 40 | 41 | (defn members 42 | "List the members in an organization. A member is a user that belongs 43 | to at least one team. If authenticated, both concealed and public members 44 | will be returned. Otherwise, only public members." 45 | [org & [options]] 46 | (api-call :get "orgs/%s/members" [org] options)) 47 | 48 | (defn member? 49 | "Check whether or not a user is a member." 50 | [org user options] 51 | (no-content? (api-call :get "orgs/%s/members/%s" [org user] options))) 52 | 53 | (defn delete-member 54 | "Remove a member from all teams and eliminate access to the organization's 55 | repositories." 56 | [org user options] 57 | (no-content? (api-call :delete "orgs/%s/members/%s" [org user] options))) 58 | 59 | ;; `members` already does this if you aren't authenticated, but for the sake of being 60 | ;; complete... 61 | (defn public-members 62 | "List the public members of an organization." 63 | [org & [options]] 64 | (api-call :get "orgs/%s/public_members" [org] options)) 65 | 66 | (defn public-member? 67 | "Check if a user is a public member or not." 68 | [org user & [options]] 69 | (no-content? (api-call :get "orgs/%s/public_members/%s" [org user] options))) 70 | 71 | (defn publicize 72 | "Make a user public." 73 | [org user options] 74 | (no-content? (api-call :put "orgs/%s/public_members/%s" [org user] options))) 75 | 76 | (defn conceal 77 | "Conceal a user's membership." 78 | [org user options] 79 | (no-content? (api-call :delete "orgs/%s/public_members/%s" [org user] options))) 80 | 81 | ;; ## Org Teams API 82 | 83 | (defn teams 84 | "List the teams for an organization." 85 | [org options] 86 | (api-call :get "orgs/%s/teams" [org] options)) 87 | 88 | (defn specific-team 89 | "Get a specific team." 90 | [id options] 91 | (api-call :get "teams/%s" [id] options)) 92 | 93 | (defn create-team 94 | "Create a team. 95 | Options are: 96 | repo-names -- Repos that belong to this team. 97 | permission -- pull (default): team can pull but not push or admin. 98 | push: team can push and pull but not admin. 99 | admin: team can push, pull, and admin." 100 | [org name options] 101 | (api-call :post "orgs/%s/teams" [org] 102 | (assoc options 103 | :name name))) 104 | 105 | (defn edit-team 106 | "Edit a team. 107 | Options are: 108 | name -- New team name. 109 | permissions -- pull (default): team can pull but not push or admin. 110 | push: team can push and pull but not admin. 111 | admin: team can push, pull, and admin." 112 | [id options] 113 | (api-call :patch "teams/%s" [id] options)) 114 | 115 | (defn delete-team 116 | "Delete a team." 117 | [id options] 118 | (no-content? (api-call :delete "teams/%s" [id] options))) 119 | 120 | (defn team-members 121 | "List members of a team." 122 | [id options] 123 | (api-call :get "teams/%s/members" [id] options)) 124 | 125 | (defn team-member? 126 | "Get a specific team member." 127 | [id user options] 128 | (no-content? (api-call :get "teams/%s/memberships/%s" [id user] options))) 129 | 130 | (defn add-team-member 131 | "Add a team member." 132 | [id user options] 133 | (no-content? (api-call :put "teams/%s/memberships/%s" [id user] options))) 134 | 135 | (defn delete-team-member 136 | "Remove a team member." 137 | [id user options] 138 | (no-content? (api-call :delete "teams/%s/memberships/%s" [id user] options))) 139 | 140 | (defn list-team-repos 141 | "List the team repositories." 142 | [id options] 143 | (api-call :get "teams/%s/repos" [id] options)) 144 | 145 | (defn team-repo? 146 | "Check if a repo is managed by this team." 147 | [id user repo options] 148 | (no-content? (api-call :get "teams/%s/repos/%s/%s" [id user repo] options))) 149 | 150 | (defn add-team-repo 151 | "Add a team repo." 152 | [id user repo options] 153 | (no-content? (api-call :put "teams/%s/repos/%s/%s" [id user repo] options))) 154 | 155 | (defn delete-team-repo 156 | "Remove a repo from a team." 157 | [id user repo options] 158 | (no-content? (api-call :delete "teams/%s/repos/%s/%s" [id user repo] options))) 159 | 160 | ;; ## Org Hooks API 161 | 162 | (defn hooks 163 | "List the hooks on an organization." 164 | [org options] 165 | (api-call :get "orgs/%s/hooks" [org] options)) 166 | 167 | (defn specific-hook 168 | "Get a specific hook." 169 | [org id options] 170 | (api-call :get "orgs/%s/hooks/%s" [org id] options)) 171 | 172 | (defn create-hook 173 | "Create a hook. 174 | Options are: 175 | events -- A sequence of event strings. Only 'push' by default. 176 | active -- true or false; determines if the hook is actually triggered 177 | on pushes." 178 | [org config options] 179 | (api-call :post "orgs/%s/hooks" [org] 180 | (assoc options 181 | :name "web" 182 | :config config))) 183 | 184 | (defn edit-hook 185 | "Edit an existing hook. 186 | Options are: 187 | config -- Modified config. 188 | events -- A sequence of event strings. Replaces the events. 189 | active -- true or false; determines if the hook is actually 190 | triggered on pushes." 191 | [org id config options] 192 | (api-call :patch "orgs/%s/hooks/%s" [org id] (assoc options :config config))) 193 | 194 | (defn ping-hook 195 | "Ping a hook." 196 | [org id options] 197 | (no-content? (api-call :post "orgs/%s/hooks/%s/pings" [org id] options))) 198 | 199 | (defn delete-hook 200 | "Delete a hook." 201 | [org id options] 202 | (no-content? (api-call :delete "orgs/%s/hooks/%s" [org id] options))) 203 | -------------------------------------------------------------------------------- /src/tentacles/search.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.search 2 | "Implements the Github Search API: http://developer.github.com/v3/search/" 3 | (:require [tentacles.core :refer [api-call]] 4 | [clojure.string :refer [join]])) 5 | 6 | (defn search-term 7 | "Builds search term based on keywords and qualifiers." 8 | [keywords & [query]] 9 | (let [separator " " 10 | gen-str (fn [k v] (str (name k) ":" v)) 11 | unwind-qualifiers (fn [[key value]] 12 | (cond 13 | (sequential? value) (->> value 14 | (map #(gen-str key %)) 15 | (join separator)) 16 | (nil? value) nil 17 | :else (gen-str key value))) 18 | joined-keywords (if (sequential? keywords) 19 | (join separator keywords) 20 | keywords)] 21 | (if (empty? query) 22 | joined-keywords 23 | (->> query 24 | (map unwind-qualifiers) 25 | (into [joined-keywords]) 26 | (filter (comp not nil?)) 27 | (join separator))))) 28 | 29 | (defn ^{:private true} search 30 | "Performs Github api call with given params." 31 | [end-point keywords query options] 32 | (api-call :get 33 | end-point 34 | nil 35 | (assoc options :q (search-term keywords query)))) 36 | 37 | (defn search-repos 38 | "Finds repositories via various criteria. This method returns up to 100 39 | results per page. 40 | 41 | Parameters are: 42 | keywords - The search keywords. Can be string or sequence of strings. 43 | query - The search qualifiers. Query is a map that contains qualifier 44 | values where key is a qualifier name. 45 | 46 | The query map can contain any combination of the supported repository 47 | search qualifiers. See full list in: 48 | https://developer.github.com/v3/search/#search-repositories 49 | 50 | Sort and order fields are available via the options map. 51 | 52 | This method follows API v3 pagination rules. More details about 53 | pagination rules in: https://developer.github.com/v3/#pagination 54 | 55 | Returns map with the following elements: 56 | :total_count - The total number of found items. 57 | :incomplete_results - true if query exceeds the time limit. 58 | :items - The result vector of found items. 59 | 60 | Example: 61 | (search-repos \"tetris\" 62 | {:language \"assembly\"} 63 | {:sort \"stars\" :order \"desc\"}) 64 | 65 | This corresponds to the following search term: 66 | https://api.github.com/search/repositories?q=tetris+language:assembly&sort=stars&order=desc" 67 | [keywords & [query options]] 68 | (search "search/repositories" keywords query options)) 69 | 70 | (defn search-code 71 | "Finds file contents via various criteria. This method returns up to 100 72 | results per page. 73 | 74 | Parameters are: 75 | keywords - The search keywords. Can be string or sequence of strings. 76 | query - The search qualifiers. Query param is a map that contains qualifier 77 | values where key is a qualifier name. 78 | 79 | The query map can contain any combination of the supported code 80 | search qualifiers. See full list in: 81 | https://developer.github.com/v3/search/#search-code 82 | 83 | Sort and order fields are available via the options map. 84 | 85 | This method follows API v3 pagination rules. More details about 86 | pagination rules in: https://developer.github.com/v3/#pagination 87 | 88 | Returns map with the following elements: 89 | :total_count - The total number of found items. 90 | :incomplete_results - true if query exceeds the time limit. 91 | :items - The result vector of found items. 92 | 93 | Example: 94 | (search-code \"addClass\" 95 | {:in \"file\" :language \"js\" :repo \"jquery/jquery\"}) 96 | 97 | This corresponds to the following search term: 98 | https://api.github.com/search/code?q=addClass+in:file+language:js+repo:jquery/jquery" 99 | [keywords & [query options]] 100 | (search "search/code" keywords query options)) 101 | 102 | (defn search-issues 103 | "Finds issues by state and keyword. This method returns up to 100 104 | results per page. 105 | 106 | Parameters are: 107 | keywords - The search keywords. Can be string or sequence of strings. 108 | query - The search qualifiers. Query is a map that contains qualifier 109 | values where key is a qualifier name. 110 | 111 | The query map can contain any combination of the supported issue 112 | search qualifiers. See full list in: 113 | https://developer.github.com/v3/search/#search-issues 114 | 115 | Sort and order fields are available via the options map. 116 | 117 | This method follows API v3 pagination rules. More details about 118 | pagination rules in: https://developer.github.com/v3/#pagination 119 | 120 | Returns map with the following elements: 121 | :total_count - The total number of found items. 122 | :incomplete_results - true if query exceeds the time limit. 123 | :items - The result vector of found items. 124 | 125 | Example: 126 | (search-issues \"windows\" 127 | {:label \"bug\" :language \"python\" :state \"open\"} 128 | {:sort \"created\" :order \"asc\"}) 129 | 130 | This corresponds to the following search term: 131 | https://api.github.com/search/issues?q=windows+label:bug+language:python+state:open&sort=created&order=asc" 132 | [keywords & [query options]] 133 | (search "search/issues" keywords query options)) 134 | 135 | (defn search-users 136 | "Finds users via various criteria. This method returns up to 100 137 | results per page. 138 | 139 | Parameters are: 140 | keywords - The search keywords. Can be string or sequence of strings. 141 | query - The search qualifiers. Query is a map that contains qualifier 142 | values where key is a qualifier name. 143 | 144 | The query map can contain any combination of the supported user 145 | search qualifiers. See full list in: 146 | https://developer.github.com/v3/search/#search-users 147 | 148 | Sort and order fields are available via the options map. 149 | 150 | This method follows API v3 pagination rules. More details about 151 | pagination rules in: https://developer.github.com/v3/#pagination 152 | 153 | Returns map with the following elements: 154 | :total_count - The total number of found items. 155 | :incomplete_results - true if query exceeds the time limit. 156 | :items - The result vector of found items. 157 | 158 | Example: 159 | (search-users \"tom\" {:repos \">42\" :followers \">1000\"}) 160 | 161 | This corresponds to the following search term: 162 | https://api.github.com/search/users?q=tom+repos:%3E42+followers:%3E1000" 163 | [keywords & [query options]] 164 | (search "search/users" keywords query options)) 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Dependencies Status](https://jarkeeper.com/Raynes/tentacles/status.svg)](https://jarkeeper.com/Raynes/tentacles) 2 | 3 | # An octocat is nothing without her tentacles 4 | 5 | Tentacles is a Clojure library for working with the Github v3 API. It supports the entire Github API. 6 | 7 | This library is the successor to my old [clj-github](https://github.com/Raynes/clj-github) library. clj-github will no longer be maintained. 8 | 9 | ## Usage 10 | 11 | This is on clojars, of course. Just add `[tentacles "0.5.1"]` to your `:dependencies` in your project.clj file. 12 | 13 | ### CODE! 14 | 15 | The library is very simple. It is a very light wrapper around the Github API. For the most part, it replaces keywords with properly formatted keywords, generates JSON for you, etc. Let's try out a few things. 16 | 17 | ```clojure 18 | user> (user-repos "amalloy") 19 | ; Evaluation aborted. 20 | user> (repos/user-repos "amalloy") 21 | [{:fork false, :pushed_at "2010-12-10T07:37:44Z", :name "ddsolve", :clone_url "https://github.com/amalloy/ddsolve.git", :watchers 1, :updated_at "2011-10-04T02:51:53Z", :html_url "https://github.com/amalloy/ddsolve", :owner {:avatar_url "https://secure.gravatar.com/avatar/1c6d7ce3810fd23f0823bf1df5103cd3?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", :url "https://api.github.com/users/amalloy", :gravatar_id "1c6d7ce3810fd23f0823bf1df5103cd3", :login "amalloy", :id 368685}, :language "Clojure", :size 1704, :created_at "2010-08-18T16:37:47Z", :private false, :homepage "", :git_url "git://github.com/amalloy/ddsolve.git", :url "https://api.github.com/repos/amalloy/ddsolve", :master_branch nil, :ssh_url "git@github.com:amalloy/ddsolve.git", :open_issues 0, :id 846605, :forks 1, :svn_url "https://svn.github.com/amalloy/ddsolve", :description "Double-dummy solver for contract bridge"} ...] 22 | ``` 23 | 24 | I cut out most of the output there. If you try it yourself, you'll notice that it produces a *ton* of output. How can we limit the output? Easily! 25 | 26 | ```clojure 27 | user> (repos/user-repos "amalloy" {:per-page 1}) 28 | [{:fork false, :pushed_at "2010-12-10T07:37:44Z", :name "ddsolve", :clone_url "https://github.com/amalloy/ddsolve.git", :watchers 1, :updated_at "2011-10-04T02:51:53Z", :html_url "https://github.com/amalloy/ddsolve", :owner {:avatar_url "https://secure.gravatar.com/avatar/1c6d7ce3810fd23f0823bf1df5103cd3?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", :url "https://api.github.com/users/amalloy", :login "amalloy", :gravatar_id "1c6d7ce3810fd23f0823bf1df5103cd3", :id 368685}, :language "Clojure", :size 1704, :created_at "2010-08-18T16:37:47Z", :private false, :homepage "", :git_url "git://github.com/amalloy/ddsolve.git", :url "https://api.github.com/repos/amalloy/ddsolve", :master_branch nil, :ssh_url "git@github.com:amalloy/ddsolve.git", :open_issues 0, :id 846605, :forks 1, :svn_url "https://svn.github.com/amalloy/ddsolve", :description "Double-dummy solver for contract bridge"}] 29 | ``` 30 | 31 | This time we actually *did* get just one item. We explicitly set the number of items allowed per page to 1. The maximum we can set that to is 100 and the default is 30. We can get specific pages of output the same way by using the `:page` option. Additionally, :all-pages true can be passed, which will return a lazy seq of all items on all pages. 32 | 33 | This also introduces an idiom in tentacles: options are a map passed to the last parameter of an API function. The options map also contains authentication data when we need it to: 34 | 35 | ```clojure 36 | user> (repos/repos {:auth "Raynes:REDACTED" :per-page 1}) 37 | [{:fork true, :pushed_at "2011-09-21T05:37:17Z", :name "lein-marginalia", :clone_url "https://github.com/Raynes/lein-marginalia.git", :watchers 1, :updated_at "2011-11-23T03:27:47Z", :html_url "https://github.com/Raynes/lein-marginalia", :owner {:login "Raynes", :avatar_url "https://secure.gravatar.com/avatar/54222b6321f0504e0a312c24e97adfc1?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png", :url "https://api.github.com/users/Raynes", :gravatar_id "54222b6321f0504e0a312c24e97adfc1", :id 54435}, :language "Clojure", :size 180, :created_at "2011-11-23T03:27:47Z", :private false, :homepage "", :git_url "git://github.com/Raynes/lein-marginalia.git", :url "https://api.github.com/repos/Raynes/lein-marginalia", :master_branch nil, :ssh_url "git@github.com:Raynes/lein-marginalia.git", :open_issues 0, :id 2832999, :forks 0, :svn_url "https://svn.github.com/Raynes/lein-marginalia", :description "A Marginalia plugin to Leiningen "}] 38 | ``` 39 | 40 | Default options can be specified via `with-defaults`. 41 | 42 | If an API function has no options and authentication would have no uses for that particular call, the options map is not a parameter at all. For API calls that can do different things based on whether or not you are authenticated but authentication is not **required**, then the options map will be an optional argument. For API calls that require authentication to function at all, the options map is a required argument. Any data that is required by an API call is a positional argument to the API functions. The options map only ever contains authentication info and/or optional input. 43 | 44 | Authentication is supported by Github user authentication `:auth ` as demonstrated above, or by oauth or oauth2. For oauth use `:oauth-token ` instead of `:auth` in the options map. Likewise, for oauth2, include `:client-id :client-token ` in the options map. 45 | 46 | You can access useful information returned by the API such as current 47 | rate limits, etags, etc. by checking the response with `core/api-meta`. You can then use this to perform conditional requests against the API. If the data has not changed, the keyword `:tentacles.core/not-modified` will be returned. This does not consume any API call quota. 48 | 49 | ```clojure 50 | user> (core/api-meta (repos/readme "Raynes" "tentacles" {})) 51 | {:links {nil nil}, :etag "\"f1f3cfabbf0f98e0bbaa7aa424f92e75\"", :last-modified "Mon, 28 Jan 2013 21:13:48 GMT", :call-limit 60, :call-remaining 59} 52 | 53 | user> (repos/readme "Raynes" "tentacles" {:etag "\"f1f3cfabbf0f98e0bbaa7aa424f92e75\""}) 54 | :tentacles.core/not-modified 55 | 56 | user> (repos/readme "Raynes" "tentacles" {:if-modified-since "Mon, 28 Jan 2013 21:13:48 GMT"}) 57 | :tentacles.core/not-modified 58 | ``` 59 | 60 | Similarly, you can set an User-Agent to make your requests more friendly and identifiable. 61 | 62 | ```clojure 63 | user> (repos/readme "Raynes" "tentacles" {:user-agent "MyPhoneApp"}) 64 | ``` 65 | 66 | The Github API is massive and great. I can't demonstrate every API call. Everything is generally just as easy as the above examples, and I'm working hard to document things as well as possible, so go explore! 67 | 68 | Here are some lovely [Marginalia docs](http://raynes.github.com/tentacles). I also wrote a demonstrational [blog post](http://blog.raynes.me/blog/2011/12/02/waving-our-tentacles/) about Tentacles that I intend to keep updated with future releases. 69 | 70 | If you run into something that isn't documented well or you don't understand, look for the API call on the Github API [docs](http://developer.github.com/v3/). If you feel like it, please submit a pull request with improved documentation. Let's make this the most impressive Github API library around! 71 | 72 | ## Hacking 73 | 74 | ### Running the tests 75 | 76 | In order to run the tests, you need to create a `testinfo.clj` in the root of the checkout with some info required for the tests to run properly. This file is ignored by git, so don't worry about committing auth info. This file should contain a Clojure map like the following: 77 | 78 | ```clojure 79 | {:user "" ;; Github username 80 | :pass "" ;; Github password 81 | :follows ""} ;; Username of a person that this user follows. 82 | ``` 83 | 84 | As more tests are written this information may grow. 85 | 86 | ## License 87 | 88 | Copyright (C) 2011 Anthony Grimes 89 | 90 | Distributed under the Eclipse Public License, the same as Clojure. 91 | -------------------------------------------------------------------------------- /src/tentacles/issues.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.issues 2 | "Implements the Github Issues API: http://developer.github.com/v3/issues/" 3 | (:use [tentacles.core :only [api-call no-content?]] 4 | [clojure.string :only [join]])) 5 | 6 | ;; Some API requests, namely GET ones, require that labels be passed as a 7 | ;; comma-delimited string of labels. The POST requests want it to be passed 8 | ;; as a list of strings. In order to be consistent, users will always pass 9 | ;; a list of labels. This joins the labels so that the string requirement 10 | ;; on GETs is transparent to the user. 11 | (defn- join-labels [m] 12 | (if (:labels m) 13 | (update-in m [:labels] (partial join ",")) 14 | m)) 15 | 16 | ;; ## Primary Issue API 17 | 18 | (defn my-issues 19 | "List issues for (authenticated) user. 20 | Options are: 21 | filter -- assigned: assigned to you, 22 | created: created by you, 23 | mentioned: issues that mention you, 24 | subscribed: issues that you're subscribed to. 25 | state -- open (default), closed. 26 | labels -- A string of comma-separated label names. 27 | sort -- created (default), updated, comments. 28 | direction -- asc: ascending, 29 | desc (default): descending. 30 | since -- String ISO 8601 timestamp." 31 | [options] 32 | (api-call :get "issues" nil (join-labels options))) 33 | 34 | (defn issues 35 | "List issues for a repository. 36 | Options are: 37 | milestone -- Milestone number, 38 | none: no milestone, 39 | *: any milestone. 40 | assignee -- A username, 41 | none: no assigned user, 42 | *: any assigned user. 43 | mentioned -- A username. 44 | state -- open (default), closed. 45 | labels -- A string of comma-separated label names. 46 | sort -- created (default), updated, comments. 47 | direction -- asc: ascending, 48 | desc (default): descending. 49 | since -- String ISO 8601 timestamp." 50 | [user repo & [options]] 51 | (api-call :get "repos/%s/%s/issues" [user repo] (join-labels options))) 52 | 53 | (defn org-issues 54 | "List all issues for a given organization for (authenticated) user. 55 | Options are: 56 | filter -- assigned: assigned to you, 57 | created: created by you, 58 | mentioned: issues that mention you, 59 | subscribed: issues that you're subscribed to. 60 | state -- open (default), closed. 61 | labels -- A string of comma-separated label names. 62 | sort -- created (default), updated, comments. 63 | direction -- asc: ascending, 64 | desc (default): descending. 65 | since -- String ISO 8601 timestamp." 66 | [org & [options]] 67 | (api-call :get "orgs/%s/issues" [org] (join-labels options))) 68 | 69 | (defn specific-issue 70 | "Fetch a specific issue." 71 | [user repo number & [options]] 72 | (api-call :get "repos/%s/%s/issues/%s" [user repo number] options)) 73 | 74 | (defn create-issue 75 | "Create an issue. 76 | Options are: 77 | milestone -- Milestone number to associate with this issue.. 78 | assignee -- A username to assign to this issue. 79 | labels -- A list of labels to associate with this issue. 80 | body -- The body text of the issue." 81 | [user repo title options] 82 | (api-call :post "repos/%s/%s/issues" [user repo] (assoc options :title title))) 83 | 84 | (defn edit-issue 85 | "Edit an issue. 86 | Options are: 87 | milestone -- Milestone number to associate with this issue.. 88 | assignee -- A username to assign to this issue. 89 | labels -- A list of labels to associate with this issue. 90 | Replaces the existing labels. 91 | state -- open or closed. 92 | title -- Title of the issue. 93 | body -- The body text of the issue." 94 | [user repo id options] 95 | (api-call :patch "repos/%s/%s/issues/%s" [user repo id] options)) 96 | 97 | ;; ## Issue Comments API 98 | 99 | (defn repo-issue-comments 100 | "List issue comments in a repository." 101 | [user repo & [options]] 102 | (api-call :get "repos/%s/%s/issues/comments" [user repo] options)) 103 | 104 | (defn issue-comments 105 | "List comments on an issue." 106 | [user repo id & [options]] 107 | (api-call :get "repos/%s/%s/issues/%s/comments" [user repo id] options)) 108 | 109 | (defn specific-comment 110 | "Get a specific comment." 111 | [user repo comment-id & [options]] 112 | (api-call :get "repos/%s/%s/issues/comments/%s" [user repo comment-id] options)) 113 | 114 | (defn create-comment 115 | "Create a comment." 116 | [user repo id body options] 117 | (api-call :post "repos/%s/%s/issues/%s/comments" 118 | [user repo id] (assoc options :body body))) 119 | 120 | (defn edit-comment 121 | "Edit a comment." 122 | [user repo comment-id body options] 123 | (api-call :patch "repos/%s/%s/issues/comments/%s" 124 | [user repo comment-id] (assoc options :body body))) 125 | 126 | (defn delete-comment 127 | "Delete a comment." 128 | [user repo comment-id options] 129 | (no-content? 130 | (api-call :delete "repos/%s/%s/issues/comments/%s" 131 | [user repo comment-id] options))) 132 | 133 | ;; ## Issue Event API 134 | 135 | (defn issue-events 136 | "List events for an issue." 137 | [user repo id & [options]] 138 | (api-call :get "repos/%s/%s/issues/%s/events" [user repo id] options)) 139 | 140 | (defn repo-events 141 | "List events for a repository." 142 | [user repo & [options]] 143 | (api-call :get "repos/%s/%s/issues/events" [user repo] options)) 144 | 145 | (defn specific-event 146 | "Get a single, specific event." 147 | [user repo id & [options]] 148 | (api-call :get "repos/%s/%s/issues/events/%s" [user repo id] options)) 149 | 150 | ;; ## Issue Label API 151 | 152 | (defn repo-labels 153 | "List labels for a repo." 154 | [user repo & [options]] 155 | (api-call :get "repos/%s/%s/labels" [user repo] options)) 156 | 157 | (defn issue-labels 158 | "List labels on an issue." 159 | [user repo issue-id & [options]] 160 | (api-call :get "repos/%s/%s/issues/%s/labels" [user repo issue-id] options)) 161 | 162 | (defn specific-label 163 | "Get a specific label." 164 | [user repo id & [options]] 165 | (api-call :get "repos/%s/%s/labels/%s" [user repo id] options)) 166 | 167 | (defn create-label 168 | "Create a label." 169 | [user repo name color options] 170 | (api-call :post "repos/%s/%s/labels" 171 | [user repo] (assoc options :name name :color color))) 172 | 173 | (defn edit-label 174 | "Edit a label." 175 | [user repo id name color options] 176 | (api-call :patch "repos/%s/%s/labels/%s" 177 | [user repo id] (assoc options :name name :color color))) 178 | 179 | (defn delete-label 180 | "Delete a label." 181 | [user repo id options] 182 | (no-content? (api-call :delete "repos/%s/%s/labels/%s" [user repo id] options))) 183 | 184 | (defn add-labels 185 | "Add labels to an issue." 186 | [user repo issue-id labels options] 187 | (api-call :post "repos/%s/%s/issues/%s/labels" 188 | [user repo issue-id] (assoc options :raw labels))) 189 | 190 | (defn remove-label 191 | "Remove a label from an issue." 192 | [user repo issue-id label-id options] 193 | (api-call :delete "repos/%s/%s/issues/%s/labels/%s" 194 | [user repo issue-id label-id] options)) 195 | 196 | (defn replace-labels 197 | "Replace all labels for an issue." 198 | [user repo issue-id labels options] 199 | (api-call :put "repos/%s/%s/issues/%s/labels" 200 | [user repo issue-id] (assoc options :raw labels))) 201 | 202 | (defn remove-all-labels 203 | "Remove all labels from an issue." 204 | [user repo issue-id options] 205 | (no-content? (api-call :delete "repos/%s/%s/issues/%s/labels" [user repo issue-id] options))) 206 | 207 | (defn milestone-labels 208 | "Get labels for every issue in a milestone." 209 | [user repo stone-id & [options]] 210 | (api-call :get "repos/%s/%s/milestones/%s/labels" [user repo stone-id] options)) 211 | 212 | ;; ## Issue Milestones API 213 | 214 | (defn repo-milestones 215 | "List milestones for a repository. 216 | Options are: 217 | state -- open (default), closed. 218 | direction -- asc, desc (default). 219 | sort -- due_date (default), completeness." 220 | [user repo & [options]] 221 | (api-call :get "repos/%s/%s/milestones" [user repo] options)) 222 | 223 | (defn specific-milestone 224 | "Get a specific milestone." 225 | [user repo id & [options]] 226 | (api-call :get "repos/%s/%s/milestones/%s" [user repo id] options)) 227 | 228 | (defn create-milestone 229 | "Create a milestone. 230 | Options are: 231 | state -- open (default), closed. 232 | description -- a description string. 233 | due-on -- String ISO 8601 timestamp" 234 | [user repo title options] 235 | (api-call :post "repos/%s/%s/milestones" 236 | [user repo] (assoc options :title title))) 237 | 238 | (defn edit-milestone 239 | "Edit a milestone. 240 | Options are: 241 | state -- open (default), closed. 242 | description -- a description string. 243 | due-on -- String ISO 8601 timestamp" 244 | [user repo id title options] 245 | (api-call :patch "repos/%s/%s/milestones/%s" 246 | [user repo id] (assoc options :title title))) 247 | 248 | (defn delete-milestone 249 | "Delete a milestone." 250 | [user repo id options] 251 | (no-content? (api-call :delete "repos/%s/%s/milestones/%s" [user repo id] options))) 252 | -------------------------------------------------------------------------------- /src/tentacles/repos.clj: -------------------------------------------------------------------------------- 1 | (ns tentacles.repos 2 | "Implements the Github Repos API: http://developer.github.com/v3/repos/" 3 | (:refer-clojure :exclude [keys]) 4 | (:require [clojure.data.codec.base64 :as b64]) 5 | (:use [clj-http.client :only [post put]] 6 | [clojure.java.io :only [file]] 7 | [tentacles.core :only [api-call no-content? raw-api-call]] 8 | [cheshire.core :only [generate-string]])) 9 | 10 | ;; ## Primary Repos API 11 | 12 | (defn all-repos 13 | "Lists all of the repositories, in the order they were created. 14 | Options are: 15 | since -- integer ID of the last repository seen." 16 | [& [options]] 17 | (api-call :get "repositories" nil options)) 18 | 19 | (defn repos 20 | "List the authenticated user's repositories. 21 | Options are: 22 | type -- all (default), public, private, member." 23 | [options] 24 | (api-call :get "user/repos" nil options)) 25 | 26 | (defn user-repos 27 | "List a user's repositories. 28 | Options are: 29 | types -- all (default), public, private, member." 30 | [user & [options]] 31 | (api-call :get "users/%s/repos" [user] options)) 32 | 33 | (defn org-repos 34 | "List repositories for an organization. 35 | Options are: 36 | type -- all (default), public, private." 37 | [org & [options]] 38 | (api-call :get "orgs/%s/repos" [org] options)) 39 | 40 | (defn create-repo 41 | "Create a new repository. 42 | Options are: 43 | description -- Repository's description. 44 | homepage -- Link to repository's homepage. 45 | public -- true (default), false. 46 | has-issues -- true (default), false. 47 | has-wiki -- true (default), false. 48 | has-downloads -- true (default), false." 49 | [name options] 50 | (api-call :post "user/repos" nil (assoc options :name name))) 51 | 52 | (defn create-org-repo 53 | "Create a new repository in an organization.. 54 | Options are: 55 | description -- Repository's description. 56 | homepage -- Link to repository's homepage. 57 | public -- true (default), false. 58 | has-issues -- true (default), false. 59 | has-wiki -- true (default), false. 60 | has-downloads -- true (default), false. 61 | team-id -- Team that will be granted access to this 62 | repository." 63 | [org name options] 64 | (api-call :post "orgs/%s/repos" [org] (assoc options :name name))) 65 | 66 | (defn specific-repo 67 | "Get a repository." 68 | [user repo & [options]] 69 | (api-call :get "repos/%s/%s" [user repo] options)) 70 | 71 | (defn edit-repo 72 | "Edit a repository. 73 | Options are: 74 | description -- Repository's description. 75 | name -- Repository's name. 76 | homepage -- Link to repository's homepage. 77 | public -- true, false. 78 | has-issues -- true, false. 79 | has-wiki -- true, false. 80 | has-downloads -- true, false." 81 | [user repo options] 82 | (api-call :patch "repos/%s/%s" 83 | [user repo] 84 | (if (:name options) 85 | options 86 | (assoc options :name repo)))) 87 | 88 | (defn contributors 89 | "List the contributors for a project. 90 | Options are: 91 | anon -- true, false (default): If true, include 92 | anonymous contributors." 93 | [user repo & [options]] 94 | (api-call :get "repos/%s/%s/contributors" [user repo] options)) 95 | 96 | (defn languages 97 | "List the languages that a repository uses." 98 | [user repo & [options]] 99 | (api-call :get "repos/%s/%s/languages" [user repo] options)) 100 | 101 | (defn teams 102 | "List a repository's teams." 103 | [user repo & [options]] 104 | (api-call :get "repos/%s/%s/teams" [user repo] options)) 105 | 106 | (defn tags 107 | "List a repository's tags." 108 | [user repo & [options]] 109 | (api-call :get "repos/%s/%s/tags" [user repo] options)) 110 | 111 | (defn branches 112 | "List a repository's branches." 113 | [user repo & [options]] 114 | (api-call :get "repos/%s/%s/branches" [user repo] options)) 115 | 116 | ;; ## Repo Collaborators API 117 | 118 | (defn collaborators 119 | "List a repository's collaborators." 120 | [user repo & [options]] 121 | (api-call :get "repos/%s/%s/collaborators" [user repo] options)) 122 | 123 | (defn collaborator? 124 | "Check if a user is a collaborator." 125 | [user repo collaborator & [options]] 126 | (no-content? (api-call :get "repos/%s/%s/collaborators/%s" [user repo collaborator] options))) 127 | 128 | (defn add-collaborator 129 | "Add a collaborator to a repository." 130 | [user repo collaborator options] 131 | (no-content? (api-call :put "repos/%s/%s/collaborators/%s" [user repo collaborator] options))) 132 | 133 | (defn remove-collaborator 134 | "Remove a collaborator from a repository." 135 | [user repo collaborator options] 136 | (no-content? (api-call :delete "repos/%s/%s/collaborators/%s" [user repo collaborator] options))) 137 | 138 | ;; ## Repo Commits API 139 | 140 | (defn commits 141 | "List commits for a repository. 142 | Options are: 143 | sha -- Sha or branch to start lising commits from. 144 | path -- Only commits at this path will be returned." 145 | [user repo & [options]] 146 | (api-call :get "repos/%s/%s/commits" [user repo] options)) 147 | 148 | (defn specific-commit 149 | "Get a specific commit." 150 | [user repo sha & [options]] 151 | (api-call :get "repos/%s/%s/commits/%s" [user repo sha] options)) 152 | 153 | (defn commit-comments 154 | "List the commit comments for a repository." 155 | [user repo & [options]] 156 | (api-call :get "repos/%s/%s/comments" [user repo] options)) 157 | 158 | (defn specific-commit-comments 159 | "Get the comments on a specific commit." 160 | [user repo sha & [options]] 161 | (api-call :get "repos/%s/%s/commits/%s/comments" [user repo sha] options)) 162 | 163 | ;; 'line' is supposed to be a required argument for this API call, but 164 | ;; I'm convinced that it doesn't do anything. The only thing that seems 165 | ;; to matter is the 'position' argument. As a matter of fact, we can omit 166 | ;; 'line' entirely and Github does not complain, despite it supposedly being 167 | ;; a required argument. 168 | ;; 169 | ;; Furthermore, it requires that the sha be passed in the URL *and* the JSON 170 | ;; input. I don't see how they can ever possibly be different, so we're going 171 | ;; to just require one sha. 172 | (defn create-commit-comment 173 | "Create a commit comment. path is the location of the file you're commenting on. 174 | position is the index of the line you're commenting on. Not the actual line number, 175 | but the nth line shown in the diff." 176 | [user repo sha path position body options] 177 | (api-call :post "repos/%s/%s/commits/%s/comments" [user repo sha] 178 | (assoc options 179 | :body body 180 | :commit-id sha 181 | :path path 182 | :position position))) 183 | 184 | (defn specific-commit-comment 185 | "Get a specific commit comment." 186 | [user repo id & [options]] 187 | (api-call :get "repos/%s/%s/comments/%s" [user repo id] options)) 188 | 189 | (defn update-commit-comment 190 | "Update a commit comment." 191 | [user repo id body options] 192 | (api-call :post "repos/%s/%s/comments/%s" [user repo id] (assoc options :body body))) 193 | 194 | (defn compare-commits 195 | [user repo base head & [options]] 196 | (api-call :get "repos/%s/%s/compare/%s...%s" [user repo base head] options)) 197 | 198 | (defn delete-commit-comment 199 | [user repo id options] 200 | (no-content? (api-call :delete "repos/%s/%s/comments/%s" [user repo id] options))) 201 | 202 | ;; ## Repo Downloads API 203 | 204 | (defn downloads 205 | "List the downloads for a repository." 206 | [user repo & [options]] 207 | (api-call :get "repos/%s/%s/downloads" [user repo] options)) 208 | 209 | (defn specific-download 210 | "Get a specific download." 211 | [user repo id & [options]] 212 | (api-call :get "repos/%s/%s/downloads/%s" [user repo id] options)) 213 | 214 | (defn delete-download 215 | "Delete a download" 216 | [user repo id options] 217 | (no-content? (api-call :delete "repos/%s/%s/downloads/%s" [user repo id] options))) 218 | 219 | ;; Github uploads are a two step process. First we get a download resource and then 220 | ;; we use that to upload the file. 221 | (defn download-resource 222 | "Get a download resource for a file you want to upload. You can pass it 223 | to upload-file to actually upload your file." 224 | [user repo path options] 225 | (let [path (file path)] 226 | (assoc (api-call :post "repos/%s/%s/downloads" 227 | [user repo] 228 | (assoc options 229 | :name (.getName path) 230 | :size (.length path))) 231 | :filepath path))) 232 | 233 | ;; This isn't really even a Github API call, since it calls an Amazon API. 234 | ;; As such, it doesn't provide the same guarentees as the rest of the API. 235 | ;; We'll just return the raw response. 236 | (defn upload-file 237 | "Upload a file given a download resource obtained from download-resource." 238 | [resp] 239 | (post (:s3_url resp) 240 | {:multipart [["key" (:path resp)] 241 | ["acl" (:acl resp)] 242 | ["success_action_status" "201"] 243 | ["Filename" (:name resp)] 244 | ["AWSAccessKeyId" (:accesskeyid resp)] 245 | ["Policy" (:policy resp)] 246 | ["Signature" (:signature resp)] 247 | ["Content-Type" (:mime_type resp)] 248 | ["file" (:filepath resp)]]})) 249 | 250 | ;; Repo Forks API 251 | 252 | (defn forks 253 | "Get a list of a repository's forks." 254 | [user repo & [options]] 255 | (api-call :get "repos/%s/%s/forks" [user repo] options)) 256 | 257 | (defn create-fork 258 | "Create a new fork. 259 | Options are: 260 | org -- If present, the repo is forked to this organization." 261 | [user repo options] 262 | (api-call :post "repos/%s/%s/forks" [user repo] options)) 263 | 264 | ;; Repo Deploy Keys API 265 | 266 | (defn keys 267 | "List deploy keys for a repo." 268 | [user repo options] 269 | (api-call :get "repos/%s/%s/keys" [user repo] options)) 270 | 271 | (defn specific-key 272 | "Get a specific deploy key." 273 | [user repo id options] 274 | (api-call :get "repos/%s/%s/keys/%s" [user repo id] options)) 275 | 276 | (defn create-key 277 | "Create a new deploy key." 278 | [user repo title key options] 279 | (api-call :post "repos/%s/%s/keys" [user repo] 280 | (assoc options :title title :key key))) 281 | 282 | (defn delete-key 283 | "Delete a deploy key." 284 | [user repo id options] 285 | (api-call :delete "repos/%s/%s/keys/%s" [user repo id] options)) 286 | 287 | ;; Repo Watcher API 288 | 289 | (defn watchers 290 | "List a repository's watchers." 291 | [user repo & [options]] 292 | (api-call :get "repos/%s/%s/watchers" [user repo] options)) 293 | 294 | (defn watching 295 | "List all the repositories that a user is watching." 296 | [user & [options]] 297 | (api-call :get "users/%s/watched" [user] options)) 298 | 299 | (defn watching? 300 | "Check if you are watching a repository." 301 | [user repo options] 302 | (no-content? (api-call :get "user/watched/%s/%s" [user repo] options))) 303 | 304 | (defn watch 305 | "Watch a repository." 306 | [user repo options] 307 | (no-content? (api-call :put "user/watched/%s/%s" [user repo] options))) 308 | 309 | (defn unwatch 310 | "Unwatch a repository." 311 | [user repo options] 312 | (no-content? (api-call :delete "user/watched/%s/%s" [user repo] options))) 313 | 314 | ;; ## Repo Stargazers 315 | 316 | (defn stargazers 317 | "List a repository's stargazers." 318 | [user repo & [options]] 319 | (api-call :get "repos/%s/%s/stargazers" [user repo] options)) 320 | 321 | (defn starring 322 | "List all the repositories that a user is starring." 323 | [user & [options]] 324 | (api-call :get "users/%s/starred" [user] options)) 325 | 326 | (defn starring? 327 | "Check if you are watching a repository." 328 | [user repo options] 329 | (no-content? (api-call :get "user/starred/%s/%s" [user repo] options))) 330 | 331 | (defn star 332 | "Star a repository." 333 | [user repo options] 334 | (no-content? (api-call :put "user/starred/%s/%s" [user repo] options))) 335 | 336 | (defn unstar 337 | "Unstar a repository" 338 | [user repo options] 339 | (no-content? (api-call :delete "user/starred/%s/%s" [user repo] options))) 340 | 341 | ;; ## Repo Hooks API 342 | 343 | (defn hooks 344 | "List the hooks on a repository." 345 | [user repo options] 346 | (api-call :get "repos/%s/%s/hooks" [user repo] options)) 347 | 348 | (defn specific-hook 349 | "Get a specific hook." 350 | [user repo id options] 351 | (api-call :get "repos/%s/%s/hooks/%s" [user repo id] options)) 352 | 353 | (defn create-hook 354 | "Create a hook. 355 | Options are: 356 | events -- A sequence of event strings. Only 'push' by default. 357 | active -- true or false; determines if the hook is actually triggered 358 | on pushes." 359 | [user repo name config options] 360 | (api-call :post "repos/%s/%s/hooks" [user repo name config] 361 | (assoc options 362 | :name name, :config config))) 363 | 364 | (defn edit-hook 365 | "Edit an existing hook. 366 | Options are: 367 | name -- Name of the hook. 368 | config -- Modified config. 369 | events -- A sequence of event strings. Replaces the events. 370 | add_events -- A sequence of event strings to be added. 371 | remove_events -- A sequence of event strings to remove. 372 | active -- true or false; determines if the hook is actually 373 | triggered on pushes." 374 | [user repo id options] 375 | (api-call :patch "repos/%s/%s/hooks/%s" [user repo id] options)) 376 | 377 | (defn test-hook 378 | "Test a hook." 379 | [user repo id options] 380 | (no-content? (api-call :post "repos/%s/%s/hooks/%s/test" [user repo id] options))) 381 | 382 | (defn delete-hook 383 | "Delete a hook." 384 | [user repo id options] 385 | (no-content? (api-call :delete "repos/%s/%s/hooks/%s" [user repo id] options))) 386 | 387 | ;; ## PubSubHubbub 388 | 389 | (defn pubsubhubub 390 | "Create or modify a pubsubhubub subscription. 391 | Options are: 392 | secret -- A shared secret key that generates an SHA HMAC of the 393 | payload content." 394 | [user repo mode event callback & [options]] 395 | (no-content? 396 | (post "https://api.github.com/hub" 397 | (merge 398 | (when-let [oauth-token (:oauth-token options)] 399 | {:headers {"Authorization" (str "token " oauth-token)}}) 400 | {:basic-auth (:auth options) 401 | :form-params 402 | (merge 403 | {"hub.mode" mode 404 | "hub.topic" (format "https://github.com/%s/%s/events/%s" 405 | user repo event) 406 | "hub.callback" callback} 407 | (when-let [secret (:secret options)] 408 | {"hub.secret" secret}))})))) 409 | 410 | ;; ## Repo Contents API 411 | 412 | (defn- decode-b64 413 | "Decodes a base64 encoded string in a response" 414 | ([res str? path] 415 | (if (and (map? res) (= (:encoding res) "base64")) 416 | (if-let [^String encoded (get-in res path)] 417 | (if (not (empty? encoded)) 418 | (let [trimmed (.replace encoded "\n" "") 419 | raw (.getBytes trimmed "UTF-8") 420 | decoded (if (seq raw) (b64/decode raw) (byte-array)) 421 | done (if str? (String. decoded "UTF-8") decoded)] 422 | (assoc-in res path done)) 423 | res) 424 | res) 425 | res)) 426 | ([res str?] (decode-b64 res str? [:content])) 427 | ([res] (decode-b64 res false [:content]))) 428 | 429 | (defn encode-b64 [content] 430 | (String. (b64/encode (.getBytes content "UTF-8")) "UTF-8")) 431 | 432 | (defn readme 433 | "Get the preferred README for a repository. 434 | Options are: 435 | ref -- The name of the Commit/Branch/Tag. Defaults to master. 436 | str? -- Whether the content should be decoded to String. Defaults to true." 437 | [user repo {:keys [str?] :or {str? true} :as options}] 438 | (decode-b64 439 | (api-call :get "repos/%s/%s/readme" [user repo] (dissoc options :str?)) 440 | str?)) 441 | 442 | (defn contents 443 | "Get the contents of any file or directory in a repository. 444 | Options are: 445 | ref -- The name of the Commit/Branch/Tag. Defaults to master. 446 | str? -- Whether the content should be decoded to a String. Defaults to false (ByteArray)." 447 | [user repo path {:keys [str?] :as options}] 448 | (decode-b64 449 | (api-call :get "repos/%s/%s/contents/%s" [user repo path] (dissoc options :str?)) 450 | str?)) 451 | 452 | (defn update-contents 453 | "Update a file in a repository 454 | path -- The content path. 455 | message -- The commit message. 456 | content -- The updated file content, Base64 encoded. 457 | sha -- The blob SHA of the file being replaced. 458 | Options are: 459 | branch -- The branch name. Default: the repository’s default branch (usually master) 460 | author -- A map containing :name and :email for the author of the commit 461 | committer -- A map containing :name and :email for the committer of the commit" 462 | [user repo path message content sha & [options]] 463 | (let [body (merge {:message message 464 | :content (encode-b64 content) 465 | :sha sha} 466 | options)] 467 | (api-call :put "repos/%s/%s/contents/%s" [user repo path] body))) 468 | 469 | (defn delete-contents 470 | "Delete a file in a repository 471 | path -- The content path. 472 | message -- The commit message. 473 | sha -- The blob SHA of the file being deleted. 474 | Options are: 475 | branch -- The branch name. Default: the repository’s default branch (usually master) 476 | author -- A map containing :name and :email for the author of the commit 477 | committer -- A map containing :name and :email for the committer of the commit" 478 | [user repo path message sha & [options]] 479 | (let [body (merge {:message message 480 | :sha sha} 481 | options)] 482 | (api-call :delete "repos/%s/%s/contents/%s" [user repo path] body))) 483 | 484 | (defn archive-link 485 | "Get a URL to download a tarball or zipball archive for a repository. 486 | Options are: 487 | ref -- The name of the Commit/Branch/Tag. Defaults to master." 488 | ([user repo archive-format {git-ref :ref :or {git-ref ""} :as options}] 489 | (let [proper-options (-> options 490 | (assoc :follow-redirects false) 491 | (dissoc :ref)) 492 | resp (raw-api-call :get "repos/%s/%s/%s/%s" [user repo archive-format git-ref] proper-options)] 493 | (if (= (resp :status) 302) 494 | (get-in resp [:headers "location"]) 495 | resp)))) 496 | 497 | ;; ## Status API 498 | (def combined-state-opt-in "application/vnd.github.she-hulk-preview+json") 499 | 500 | (defn statuses 501 | "Returns the combined status of a ref (SHA, branch, or tag). 502 | By default, returns the combined status. Include `:combined? false' 503 | in options to disable combined status 504 | (see https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref)" 505 | [user repo ref & [options]] 506 | (let [combined? (:combined? options true)] 507 | (api-call :get 508 | (if combined? 509 | "repos/%s/%s/commits/%s/status" 510 | "repos/%s/%s/statuses/%s") 511 | [user repo ref] 512 | (cond-> options 513 | combined? (assoc :accept combined-state-opt-in))))) 514 | 515 | (defn create-status 516 | "Creates a status. 517 | Options are: state target-url description context; state is mandatory" 518 | [user repo sha options] 519 | (api-call :post "repos/%s/%s/statuses/%s" [user repo sha] 520 | (assoc options 521 | :accept combined-state-opt-in))) 522 | 523 | ;; ## Deployments API 524 | (def deployments-opt-in "application/vnd.github.cannonball-preview+json") 525 | 526 | (defn deployments 527 | "Returns deployments for a repo" 528 | [user repo & [options]] 529 | (api-call :get "repos/%s/%s/deployments" [user repo] 530 | (assoc options 531 | :accept deployments-opt-in))) 532 | 533 | (defn create-deployment 534 | "Creates a deployment for a ref. 535 | Options are: force, payload, auto-merge, description" 536 | [user repo ref options] 537 | (api-call :post "repos/%s/%s/deployments" [user repo] 538 | (assoc options 539 | :ref ref 540 | :accept deployments-opt-in))) 541 | 542 | (defn deployment-statuses 543 | "Returns deployment statuses for a deployment" 544 | [user repo deployment options] 545 | (api-call :get "repos/%s/%s/deployments/%s/statuses" [user repo deployment] 546 | (assoc options 547 | :accept deployments-opt-in))) 548 | 549 | (defn create-deployment-status 550 | "Create a deployment status. 551 | Options are: state (required), target-url, description" 552 | [user repo deployment options] 553 | (api-call :post "repos/%s/%s/deployments/%s/statuses" [user repo deployment] 554 | (assoc options 555 | :accept deployments-opt-in))) 556 | 557 | ;; # Releases api 558 | 559 | (defn releases 560 | "List releases for a repository." 561 | [user repo & [options]] 562 | (api-call :get "repos/%s/%s/releases" [user repo] options)) 563 | 564 | (defn specific-release 565 | "Gets a specific release." 566 | [user repo id & [options]] 567 | (api-call :get "repos/%s/%s/releases/%s" [user repo id] options)) 568 | 569 | (defn specific-release-by-tag 570 | "Gets a specific release by tag." 571 | [user repo tag & [options]] 572 | (api-call :get "repos/%s/%s/releases/tags/%s" [user repo tag] options)) 573 | 574 | (defn create-release 575 | "Creates a release. 576 | Options are: tag-name (required), target-commitish, name, body, draft, prerelease" 577 | [user repo options] 578 | (api-call :post "repos/%s/%s/releases" [user repo] options)) 579 | 580 | (defn delete-release 581 | "Deletes a release." 582 | [user repo id & [options]] 583 | (api-call :delete "repos/%s/%s/releases/%s" [user repo id] options)) 584 | 585 | ;; ## Statistics API 586 | 587 | (defn contributor-statistics 588 | "List additions, deletions, and commit counts per contributor" 589 | [user repo & [options]] 590 | (api-call :get "repos/%s/%s/stats/contributors" [user repo] options)) 591 | 592 | (defn commit-activity 593 | "List weekly commit activiy for the past year" 594 | [user repo & [options]] 595 | (api-call :get "repos/%s/%s/stats/commit_activity" [user repo] options)) 596 | 597 | (defn code-frequency 598 | "List weekly additions and deletions" 599 | [user repo & [options]] 600 | (api-call :get "repos/%s/%s/stats/code_frequency" [user repo] options)) 601 | 602 | (defn participation 603 | "List weekly commit count grouped by the owner and all other users" 604 | [user repo & [options]] 605 | (api-call :get "repos/%s/%s/stats/participation" [user repo] options)) 606 | 607 | (defn punch-card 608 | "List commit count per hour in the day" 609 | [user repo & [options]] 610 | (api-call :get "repos/%s/%s/stats/punch_card" [user repo] options)) 611 | --------------------------------------------------------------------------------