tentacles

0.1.2


A library for working with the Github API.

dependencies

clojure
1.3.0
clj-http
0.2.5
cheshire
2.0.2
slingshot
0.9.0



(this space intentionally left almost blank)
 

The v3 version of the Github API is very simple and consistent. We're going to take the mini-functional-DSL approach to this. We'll just abstract over API requests and write a function for every API call. This results in a simple and consistent implementation that requires no macro-magic.

(ns tentacles.core
  (:require [clj-http.client :as http]
            [cheshire.core :as json]))
(def url "https://api.github.com/")

Turn keywords into strings and replace hyphens with underscores.

The Github API expects json with underscores and such. cheshire can turn our keywords into strings, but it doesn't replace hyphens with underscores. We'll use this little interim step to do it ourselves.

(defn query-map
  [entries]
  (into {}
        (for [[k v] entries]
          [(.replace (name k) "-" "_") v])))

Same as json/parse-string but handles nil gracefully.

cheshire's parse-string explodes on nil input. We'll use this to get around that.

(defn parse-json
  [s] (when s (json/parse-string s true)))

Takes a response and checks for certain status codes. If 204, return nil. If 400, 422, 404, 204, or 500, return the original response with the body parsed as json. Otherwise, parse and return the body.

Github returns a zillion and one HTTP status codes. Pretty much all of them have JSON bodies, so in the event of something going wrong, we'll return the entire request with the :body parsed as JSON. Note that 204s will almost never be translate to the client, since those usually indicate true or false values and are reflected as such by tentacles API fns.

(defn safe-parse
  [resp]
  (if (#{400 204 422 404 500} (:status resp))
    (update-in resp [:body] parse-json)
    (parse-json (:body resp))))

Takes a response and returns true if it is a 204 response, false otherwise.

Github usually throws you 204 responses for 'true' and 404 for 'false'. We want to translate to booleans.

(defn no-content?
  [x] (= (:status x) 204))

We're using basic auth for authentication because oauth2 isn't really that easy to work with, and isn't really applicable to desktop applications (at this point, Github itself recommends basic auth for desktop apps).

Each function will pass positional arguments and possibly a map of query args or a map that will get transformed into a JSON hash and sent as the body of POST requests. The positional arguments are formatted into the URL itself, where the URL has placed %s where it needs to have the args placed. If the method is :put or :post, we generate JSON from query OR if query is a map that contains a :raw key, we generate JSON from that. This allows us to handle API calls that expect the body to not be a hash but instead be some other JSON object. Doing this allows us to be consistent in expecting a map.

The query map can also contain an :auth key (that will be removed from the map before it is used as query params or JSON data). This is either a string like "username:password" or a vector like ["username" "password"]. Authentication is basic authentication.

(defn make-request [method end-point positional query]
  (let [req (merge
             {:url (str url (apply format end-point positional))
              :basic-auth (query "auth")
              :throw-exceptions false
              :method method}
             (when (query "oauth_token")
               {:headers {"Authorization" (str "token " (query "oauth_token"))}}))]
    (safe-parse
     (http/request
      (let [proper-query (dissoc query "auth" "oauth_token")]
        (if (#{:post :put :delete} method)
          (assoc req :body (json/generate-string (or (proper-query "raw") proper-query)))
          (assoc req :query-params proper-query)))))))

Functions will call this to create API calls.

(defn api-call [method end-point positional query]
  (let [query (query-map query)]
    (make-request method end-point positional query)))
 

Implements the Git Data API: http://developer.github.com/v3/git/blobs/

(ns tentacles.data
  (:use [tentacles.core :only [api-call]]))

Blobs

Get a blob.

(defn blob
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/git/blobs/%s" [user repo sha] options))

Create a blob.

(defn create-blob
  [user repo content encoding options]
  (api-call :post "repos/%s/%s/git/blobs" [user repo]
            (assoc options
              :content content
              :encoding encoding)))

Commits

Get a commit.

(defn commit
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/git/commits/%s" [user repo sha] options))

Create a commit. Options are: parents -- A sequence of SHAs of the commits that were the parents of this commit. If omitted, the commit will be written as a root commit. author -- A map of the following (string keys): "name" -- Name of the author of the commit. "email" -- Email of the author of the commit. "date" -- Timestamp when this commit was authored. committer -- A map of the following (string keys): "name" -- Name of the committer of the commit. "email" -- Email of the committer of the commit. "date" -- Timestamp when this commit was committed. If the committer section is omitted, then it will be filled in with author data. If that is omitted, information will be obtained using the authenticated user's information and the current date.

(defn create-commit
  [user repo message tree options]
  (api-call :post "repos/%s/%s/git/commits" [user repo]
            (assoc options
              :message message
              :tree tree
              :parents parents)))

References

Get a reference.

(defn reference
  [user repo ref & [options]]
  (api-call :get "repos/%s/%s/git/refs/%s" [user repo ref] options))

Get all references.

(defn references
  [user repo & [options]]
  (api-call :get "repos/%s/%s/git/refs" [user repo] options))

Create a new reference.

(defn create-reference
  [user repo ref sha options]
  (api-call :post "repos/%s/%s/git/refs" [user repo]
            (assoc options
              :ref ref
              :sha sha)))

Edit a reference. Options are: force -- true or false (default); whether to force the update or make sure this is a fast-forward update.

(defn edit-reference
  [user repo ref sha options]
  (api-call :post "repos/%s/%s/git/refs/%s" [user repo ref]
            (assoc options :sha sha)))

Tags

Get a tag.

(defn tag
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/git/tags/%s" [user repo sha] options))

Create a tag object. Note that this does not create the reference that makes a tag in Git. If you want to create an annotated tag, you have to do this call to create the tag object and then create the refs/tags/[tag] reference. If you want to create a lightweight tag, you simply need to create the reference and this call would be unnecessary. Options are: tagger -- A map (string keys) containing the following: "name" -- Name of the author of this tag. "email" -- Email of the author of this tag. "date" -- Timestamp when this object was tagged.

The API documentation is unclear about which parts of this API call are optional.

(defn create-tag
  [user repo tag message object type options]
  (api-call :post "repos/%s/%s/git/tags" [user repo]
            (assoc options
              :tag tag
              :message message
              :object object
              :type type)))

Trees

Get a tree. Options are: recursive -- true or false; get a tree recursively?

(defn tree
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/git/trees/%s" [user repo sha] options))

Create a tree. 'tree' is a map of the following (string keys): path -- The file referenced in the tree. mode -- The file mode; one of 100644 for file, 100755 for executable, 040000 for subdirectory, 160000 for submodule, or 120000 for a blob that specifies the path of a symlink. type -- blog, tree, or commit. sha -- SHA of the object in the tree. content -- Content that you want this file to have. Options are: base-tree -- SHA of the tree you want to update (if applicable).

(defn create-tree
  [user repo tree options]
  (api-call :post "repos/%s/%s/git/trees" [user repo]
            (assoc options :tree tree)))
 

Implements the Github Events API: http://developer.github.com/v3/events/

(ns tentacles.events
  (:use [tentacles.core :only [api-call]]))

List public events.

(defn events
  []
  (api-call :get "events" nil nil))

List repository events.

(defn repo-events
  [user repo & [options]]
  (api-call :get "repos/%s/%s/events" [user repo] options))

List issue events for a repository.

(defn issue-events
  [user repo & [options]]
  (api-call :get "repos/%s/%s/issues/events" [user repo] options))

List events for a network of repositories.

(defn network-events
  [user repo & [options]]
  (api-call :get "networks/%s/%s/events" [user repo] options))

List events that a user has received. If authenticated, you'll see private events, otherwise only public.

(defn user-events
  [user & [options]]
  (api-call :get "users/%s/received_events" [user] options))

List events perofmred by a user. If you're authenticated, you'll see private events, otherwise you'll only see public events.

(defn performed-events
  [user & [options]]
  (api-call :get "users/%s/events" [user] options))

List an organization's events.

Even though this requires authentication, you still need to pass the username in the URL. I can work around this, but I don't feel like it right now.

(defn org-events
  [user org options]
  (api-call :get "users/%s/events/orgs/%s" [user org] options))
 

Implements the Github Gists API: http://developer.github.com/v3/gists/

(ns tentacles.gists
  (:use [tentacles.core :only [api-call no-content?]]))

Primary gist API

List a user's gists.

(defn user-gists
  [user & [options]]
  (api-call :get "users/%s/gists" [user] options))

If authenticated, list the authenticated user's gists. Otherwise, return all public gists.

(defn gists
  [& [options]]
  (api-call :get "gists" nil options))

List all public gists.

(defn public-gists
  []
  (api-call :get "gists/public" nil nil))

List the authenticated user's starred gists.

(defn starred-gists
  [options]
  (api-call :get "gists/starred" nil options))

Get a specific gist.

(defn specific-gist
  [id & [options]]
  (api-call :get "gists/%s" [id] options))

For whatever insane reason, Github expects gist files to be passed as a JSON hash of filename -> hash of content -> contents rather than just filename -> contents. I'm not going to be a dick and require that users of this library pass maps like that.

It does make sense in edit-gist, however, since we can selectively update a file's name and/or content, or both. I imagine that Github chose to require a subhash with a content key in the creation api end-point for consistency. In our case, I think I'd rather have a sensible gist creation function.

(defn- file-map [options files]
  (assoc options
    :files (into {} (for [[k v] files] [k {:content v}]))))

Create a gist. files is a map of filenames to contents. Options are: description -- A string description of the gist. public -- true (default) or false; whether or not the gist is public.

(defn create-gist
  [files & [options]]
  (api-call :post "gists" nil
            (assoc (file-map options files)
              :public (:public options true))))

Edit a gist. Options are: description -- A string to update the description to. files -- A map of filenames to maps. These submaps may contain either of the following, or both: a :contents key that will replace the gist's contents, and a :filename key that will replace the name of the file. If one of the file keys in the map is associated with 'nil', it'll be deleted.

It makes sense to require the user to pass :files the way Github expects it here: as a map of filenames to maps of :contents and/or :filename. It makes sense because users can selectively update only certain parts of a gist. A map is a clean way to express this update.

(defn edit-gist
  [id & [options]]
  (api-call :post "gists/%s" [id] options))

Star a gist.

(defn star-gist
  [id & [options]]
  (no-content? (api-call :put "gists/%s/star" [id] options)))

Unstar a gist.

(defn unstar-gist
  [id & [options]]
  (no-content? (api-call :delete "gists/%s/star" [id] options)))

Check if a gist is starred.

Github sends 404 which clj-http throws an exception for if a gist is not starred. I'd rather get back true or false.

(defn starred?
  [id & [options]]
  (no-content? (api-call :get "gists/%s/star" [id] options)))

Fork a gist.

(defn fork-gist
  [id & [options]]
  (api-call :post "gists/%s/fork" [id] options))

Delete a gist.

(defn delete-gist
  [id & [options]]
  (no-content? (api-call :delete "gists/%s" [id] options)))

Gist Comments API

List comments for a gist.

(defn comments
  [id & [options]]
  (api-call :get "gists/%s/comments" [id] options))

Get a specific comment.

(defn specific-comment
  [comment-id & [options]]
  (api-call :get "gists/comments/%s" [comment-id] options))

Create a comment.

(defn create-comment
  [id body options]
  (api-call :post "gists/%s/comments" [id] (assoc options :body body)))

Edit a comment.

(defn edit-comment
  [comment-id body options]
  (api-call :post "gists/comments/%s" [comment-id] (assoc options :body body)))

Delete a comment.

(defn delete-comment
  [comment-id options]
  (no-content? (api-call :delete "gists/comments/%s" [comment-id] options)))
 

Implements the Github Issues API: http://developer.github.com/v3/issues/

(ns tentacles.issues
  (:use [tentacles.core :only [api-call no-content?]]
        [clojure.string :only [join]]))

Some API requests, namely GET ones, require that labels be passed as a comma-delimited string of labels. The POST requests want it to be passed as a list of strings. In order to be consistent, users will always pass a list of labels. This joins the labels so that the string requirement on GETs is transparent to the user.

(defn- join-labels [m]
  (if (:labels m)
    (update-in m [:labels] (partial join ","))
    m))

Primary Issue API

List issues for (authenticated) user. Options are: filter -- assigned: assigned to you, created: created by you, mentioned: issues that mention you, subscribed: issues that you're subscribed to. state -- open (default), closed. labels -- A string of comma-separated label names. sort -- created (default), updated, comments. direction -- asc: ascending, desc (default): descending. since -- String ISO 8601 timestamp.

(defn my-issues
  [options]
  (api-call :get "issues" nil (join-labels options)))

List issues for a repository. Options are: milestone -- Milestone number, none: no milestone, *: any milestone. assignee -- A username, none: no assigned user, *: any assigned user. mentioned -- A username. state -- open (default), closed. labels -- A string of comma-separated label names. sort -- created (default), updated, comments. direction -- asc: ascending, desc (default): descending. since -- String ISO 8601 timestamp.

(defn issues
  [user repo & [options]]
  (api-call :get "repos/%s/%s/issues" [user repo] (join-labels options)))

Fetch a specific issue.

(defn specific-issue
  [user repo number & [options]]
  (api-call :get "repos/%s/%s/issues/%s" [user repo number] options))
(defn create-issue
  [user repo title options]
  "Create an issue.
   Options are:
     milestone -- Milestone number to associate with this issue..
     assignee  -- A username to assign to this issue.
     labels    -- A list of labels to associate with this issue.
     body      -- The body text of the issue."
  (api-call :post "repos/%s/%s/issues" [user repo] (assoc options :title title)))
(defn edit-issue
  [user repo id options]
  "Edit an issue.
   Options are:
     milestone -- Milestone number to associate with this issue..
     assignee  -- A username to assign to this issue.
     labels    -- A list of labels to associate with this issue.
                  Replaces the existing labels.
     state     -- open or closed.
     title     -- Title of the issue.
     body      -- The body text of the issue."
  (api-call :post "repos/%s/%s/issues/%s" [user repo id] options))

Issue Comments API

(defn issue-comments
  [user repo id & [options]]
  "List comments on an issue."
  (api-call :get "repos/%s/%s/issues/%s/comments" [user repo id] options))
(defn specific-comment
  [user repo comment-id & [options]]
  "Get a specific comment."
  (api-call :get "repos/%s/%s/issues/comments/%s" [user repo comment-id] options))

Create a comment.

(defn create-comment
  [user repo id body options]
  (api-call :post "repos/%s/%s/issues/%s/comments"
            [user repo id] (assoc options :body body)))

Edit a comment.

(defn edit-comment
  [user repo comment-id body options]
  (api-call :post "repos/%s/%s/issues/comments/%s"
            [user repo comment-id] (assoc options :body body)))

Delete a comment.

(defn delete-comment
  [user repo comment-id options]
  (no-content?
   (api-call :delete "repos/%s/%s/issues/comments/%s"
             [user repo comment-id] options)))

Issue Event API

List events for an issue.

(defn issue-events
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/issues/%s/events" [user repo id] options))

List events for a repository.

(defn repo-events
  [user repo & [options]]
  (api-call :get "repos/%s/%s/issues/events" [user repo] options))

Get a single, specific event.

(defn specific-event
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/issues/events/%s" [user repo id] options))

Issue Label API

List labels for a repo.

(defn repo-labels
  [user repo & [options]]
  (api-call :get "repos/%s/%s/labels" [user repo] options))

List labels on an issue.

(defn issue-labels
  [user repo issue-id & [options]]
  (api-call :get "repos/%s/%s/issues/%s/labels" [user repo issue-id] options))

Get a specific label.

(defn specific-label
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/labels/%s" [user repo id] options))

Create a label.

(defn create-label
  [user repo name color options]
  (api-call :post "repos/%s/%s/labels"
            [user repo] (assoc options :name name :color color)))

Edit a label.

(defn edit-label
  [user repo id name color options]
  (api-call :post "repos/%s/%s/labels/%s"
            [user repo id] (assoc options :name name :color color)))

Delete a label.

(defn delete-label
  [user repo id options]
  (no-content? (api-call :delete "repos/%s/%s/labels/%s" [user repo id] options)))

Add labels to an issue.

(defn add-labels
  [user repo issue-id labels options]
  (api-call :post "repos/%s/%s/issues/%s/labels"
            [user repo issue-id] (assoc options :raw labels)))

Remove a label from an issue.

(defn remove-label
  [user repo issue-id label-id options]
  (api-call :delete "repos/%s/%s/issues/%s/labels/%s"
            [user repo issue-id label-id] options))

Replace all labels for an issue.

(defn replace-labels
  [user repo issue-id labels options]
  (api-call :put "repos/%s/%s/issues/%s/labels"
            [user repo issue-id] (assoc options :raw labels)))

Remove all labels from an issue.

(defn remove-all-labels
  [user repo issue-id options]
  (no-content? (api-call :delete "repos/%s/%s/issues/%s/labels" [user repo issue-id] options)))

Get labels for every issue in a milestone.

(defn milestone-labels
  [user repo stone-id & [options]]
  (api-call :get "repos/%s/%s/milestones/%s/labels" [user repo stone-id] options))

Issue Milestones API

List milestones for a repository. Options are: state -- open (default), closed. direction -- asc, desc (default). sort -- due_date (default), completeness.

(defn repo-milestones
  [user repo & [options]]
  (api-call :get "repos/%s/%s/milestones" [user repo] options))

Get a specific milestone.

(defn specific-milestone
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/milestones/%s" [user repo id] options))

Create a milestone. Options are: state -- open (default), closed. description -- a description string. due-on -- String ISO 8601 timestamp

(defn create-milestone
  [user repo title options]
  (api-call :post "repos/%s/%s/milestones"
            [user repo] (assoc options :title title)))

Edit a milestone. Options are: state -- open (default), closed. description -- a description string. due-on -- String ISO 8601 timestamp

(defn edit-milestone
  [user repo id title options]
  (api-call :post "repos/%s/%s/milestones/%s"
            [user repo id] (assoc options :title title)))

Delete a milestone.

(defn delete-milestone
  [user repo id options]
  (no-content? (api-call :delete "repos/%s/%s/milestones/%s" [user repo id] options)))
 

Implements the Github Orgs API: http://developer.github.com/v3/orgs/

(ns tentacles.orgs
  (:use [tentacles.core :only [api-call no-content?]]))

Primary API

List the public organizations for a user.

(defn user-orgs
  [user]
  (api-call :get "users/%s/orgs" [user] nil))

List the public and private organizations for the currently authenticated user.

(defn orgs
  [options]
  (api-call :get "user/orgs" nil options))

Get a specific organization.

(defn specific-org
  [org & [options]]
  (api-call :get "orgs/%s" [org] options))

Edit an organization. Options are: billing-email -- Billing email address. company -- The name of the company the organization belongs to. email -- Publically visible email address. location -- Organization location. name -- Name of the organization.

(defn edit-org
  [org options]
  (api-call :post "orgs/%s" [org] options))

Org Members API

List the members in an organization. A member is a user that belongs to at least one team. If authenticated, both concealed and public members will be returned. Otherwise, only public members.

(defn members
  [org & [options]]
  (api-call :get "orgs/%s/members" [org] options))

Check whether or not a user is a member.

(defn member?
  [org user options]
  (no-content? (api-call :get "orgs/%s/members/%s" [org user] options)))

Remove a member from all teams and eliminate access to the organization's repositories.

(defn delete-member
  [org user options]
  (no-content? (api-call :delete "orgs/%s/members/%s" [org user] options)))

List the public members of an organization.

members already does this if you aren't authenticated, but for the sake of being complete...

(defn public-members
  [org & [options]]
  (api-call :get "orgs/%s/public_members" [org] options))

Check if a user is a public member or not.

(defn public-member?
  [org user & [options]]
  (no-content? (api-call :get "orgs/%s/public_members/%s" [org user] options)))

Make a user public.

(defn publicize
  [org user options]
  (no-content? (api-call :put "orgs/%s/public_members/%s" [org user] options)))

Conceal a user's membership.

(defn conceal
  [org user options]
  (no-content? (api-call :delete "orgs/%s/public_members/%s" [org user] options)))

Org Teams API

List the teams for an organization.

(defn teams
  [org options]
  (api-call :get "orgs/%s/teams" [org] options))

Get a specific team.

(defn specific-team
  [id options]
  (api-call :get "teams/%s" [id] options))

Create a team. Options are: repo-names -- Repos that belong to this team. permission -- pull (default): team can pull but not push or admin. push: team can push and pull but not admin. admin: team can push, pull, and admin.

(defn create-team
  [org name options]
  (api-call :post "orgs/%s/teams" [org]
            (assoc options
              :name name)))

Edit a team. Options are: name -- New team name. permissions -- pull (default): team can pull but not push or admin. push: team can push and pull but not admin. admin: team can push, pull, and admin.

(defn edit-team
  [id options]
  (api-call :post "teams/%s" [id] options))

Delete a team.

(defn delete-team
  [id options]
  (no-content? (api-call :delete "teams/%s" [id] options)))

List members of a team.

(defn team-members
  [id options]
  (api-call :get "teams/%s/members" [id] options))

Get a specific team member.

(defn team-member?
  [id user options]
  (no-content? (api-call :get "teams/%s/members/%s" [id user] options)))

Add a team member.

(defn add-team-member
  [id user options]
  (no-content? (api-call :put "teams/%s/members/%s" [id user] options)))

Remove a team member.

(defn delete-team-member
  [id user options]
  (no-content? (api-call :delete "teams/%s/members/%s" [id user] options)))

List the team repositories.

(defn list-team-repos
  [id options]
  (api-call :get "teams/%s/repos" [id] options))

Check if a repo is managed by this team.

(defn team-repo?
  [id user repo options]
  (no-content? (api-call :get "teams/%s/repos/%s/%s" [id user repo] options)))

Add a team repo.

(defn add-team-repo
  [id user repo options]
  (no-content? (api-call :put "teams/%s/repos/%s/%s" [id user repo] options)))

Remove a repo from a team.

(defn delete-team-repo
  [id user repo options]
  (no-content? (api-call :delete "teams/%s/repos/%s/%s" [id user repo] options)))
 

Implement the Github Pull Requests API: http://developer.github.com/v3/pulls/

(ns tentacles.pulls
  (:refer-clojure :exclude [merge])
  (:use [tentacles.core :only [api-call empty?]]))
(defn pulls
  "List pull requests on a repo.
   Options are:
      state -- open (default), closed."
  [user repo & [options]]
  (api-call :get "repos/%s/%s/pulls" [user repo] options))
(defn specific-pull
  "Get a specific pull request."
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/pulls/%s" [user repo id] options))
(defn create-pull
  "Create a new pull request. If from is a number, it is considered
   to be an issue number on the repository in question. If this is used,
   the pull request will be created from the existing issue. If it is a
   string it is considered to be a title. base is the branch or ref that
   you want your changes pulled into, and head is the branch or ref where
   your changes are implemented.
   Options are:
      body -- The body of the pull request text. Only applies when not
              creating a pull request from an issue."
  ([user repo from base head options]
     (api-call :post "repos/%s/%s/pulls" [user repo]
               (let [base-opts (assoc options
                                 :base base
                                 :head head)]
                 (if (number? from)
                   (assoc base-opts :issue from)
                   (assoc base-opts :title from))))))
(defn edit-pull
  "Edit a pull request.
   Options are:
      title -- a new title.
      body  -- a new body.
      state -- open or closed."
  [user repo id options]
  (api-call :post "repos/%s/%s/pulls/%s" [user repo id] options))
(defn commits
  "List the commits on a pull request."
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/pulls/%s/commits" [user repo id] options))
(defn files
  "List the files on a pull request."
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/pulls/%s/files" [user repo id] options))
(defn merged?
  "Check if a pull request has been merged."
  [user repo id & [options]]
  (empty? (api-call :get "repos/%s/%s/pulls/%s/merge" [user repo id] options)))
(defn merge
  "Merge a pull request.
   Options are:
      commit-message -- A commit message for the merge commit."
  [user repo id options]
  (api-call :put "repos/%s/%s/pulls/%s/merge" [user repo id] options))

Pull Request Comment API

(defn comments
  "List comments on a pull request."
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/pulls/%s/comments" [user repo id] options))
(defn specific-comment
  "Get a specific comment on a pull request."
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/pulls/comments/%s" [user repo id] options))

You're supposed to be able to reply to comments as well, but that doesn't seem to actually work. Commenting tha

(defn create-comment
  "Create a comment on a pull request."
  [user repo id sha path position body options]
  (api-call :post "repos/%s/%s/pulls/%s/comments" [user repo id]
            (assoc options
              :commit-id sha
              :path path
              :position position
              :body body)))
(defn edit-comment
  "Edit a comment on a pull request."
  [user repo id body options]
  (api-call :post "repos/%s/%s/pulls/comments/%s" [user repo id]
            (assoc options :body body)))
(defn delete-comment
  "Delete a comment on a pull request."
  [user repo id options]
  (empty? (api-call :delete "repos/%s/%s/pulls/comments/%s" [user repo id] options)))
 

Implements the Github Repos API: http://developer.github.com/v3/repos/

(ns tentacles.repos
  (:refer-clojure :exclude [keys])
  (:use [clj-http.client :only [post put]]
        [clojure.java.io :only [file]]
        [tentacles.core :only [api-call no-content?]]
        [cheshire.core :only [generate-string]]))

Primary Repos API

List the authenticated user's repositories. Options are: type -- all (default), public, private, member.

(defn repos
  [options]
  (api-call :get "user/repos" nil options))

List a user's repositories. Options are: types -- all (default), public, private, member.

(defn user-repos
  [user & [options]]
  (api-call :get "users/%s/repos" [user] options))

List repositories for an organization. Options are: type -- all (default), public, private.

(defn org-repos
  [org & [options]]
  (api-call :get "orgs/%s/repos" [org] options))

Create a new repository. Options are: description -- Repository's description. homepage -- Link to repository's homepage. public -- true (default), false. has-issues -- true (default), false. has-wiki -- true (default), false. has-downloads -- true (default), false.

(defn create-repo
  [name options]
  (api-call :post "user/repos" nil (assoc options :name name)))

Create a new repository in an organization.. Options are: description -- Repository's description. homepage -- Link to repository's homepage. public -- true (default), false. has-issues -- true (default), false. has-wiki -- true (default), false. has-downloads -- true (default), false. team-id -- Team that will be granted access to this repository.

(defn create-org-repo
  [org name options]
  (api-call :post "orgs/%s/repos" [org] (assoc options :name name)))

Get a repository.

(defn specific-repo
  [user repo & [options]]
  (api-call :get "repos/%s/%s" [user repo] options))

Edit a repository. Options are: description -- Repository's description. name -- Repository's name. homepage -- Link to repository's homepage. public -- true, false. has-issues -- true, false. has-wiki -- true, false. has-downloads -- true, false.

(defn edit-repo
  [user repo options]
  (api-call :post "repos/%s/%s"
            [user repo]
            (if (:name options)
              options
              (assoc options :name repo))))

List the contributors for a project. Options are: anon -- true, false (default): If true, include anonymous contributors.

(defn contributors
  [user repo & [options]]
  (api-call :get "repos/%s/%s/contributors" [user repo] options))

List the languages that a repository uses.

(defn languages
  [user repo & [options]]
  (api-call :get "repos/%s/%s/languages" [user repo] options))

List a repository's teams.

(defn teams
  [user repo & [options]]
  (api-call :get "repos/%s/%s/teams" [user repo] options))

List a repository's tags.

(defn tags
  [user repo & [options]]
  (api-call :get "repos/%s/%s/tags" [user repo] options))

List a repository's branches.

(defn branches
  [user repo & [options]]
  (api-call :get "repos/%s/%s/branches" [user repo] options))

Repo Collaborators API

List a repository's collaborators.

(defn collaborators
  [user repo & [options]]
  (api-call :get "repos/%s/%s/collaborators" [user repo] options))

Check if a user is a collaborator.

(defn collaborator?
  [user repo collaborator & [options]]
  (no-content? (api-call :get "repos/%s/%s/collaborators/%s" [user repo collaborator] options)))

Add a collaborator to a repository.

(defn add-collaborator
  [user repo collaborator options]
  (no-content? (api-call :put "repos/%s/%s/collaborators/%s" [user repo collaborator] options)))

Remove a collaborator from a repository.

(defn remove-collaborator
  [user repo collaborator options]
  (no-content? (api-call :delete "repos/%s/%s/collaborators/%s" [user repo collaborator] options)))

Repo Commits API

List commits for a repository. Options are: sha -- Sha or branch to start lising commits from. path -- Only commits at this path will be returned.

(defn commits
  [user repo & [options]]
  (api-call :get "repos/%s/%s/commits" [user repo] options))

Get a specific commit.

(defn specific-commit
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/commits/%s" [user repo sha] options))

List the commit comments for a repository.

(defn commit-comments
  [user repo & [options]]
  (api-call :get "repos/%s/%s/comments" [user repo] options))

Get the comments on a specific commit.

(defn specific-commit-comments
  [user repo sha & [options]]
  (api-call :get "repos/%s/%s/commits/%s/comments" [user repo sha] options))

Create a commit comment. path is the location of the file you're commenting on. position is the index of the line you're commenting on. Not the actual line number, but the nth line shown in the diff.

'line' is supposed to be a required argument for this API call, but I'm convinced that it doesn't do anything. The only thing that seems to matter is the 'position' argument. As a matter of fact, we can omit 'line' entirely and Github does not complain, despite it supposedly being a required argument.

Furthermore, it requires that the sha be passed in the URL and the JSON input. I don't see how they can ever possibly be different, so we're going to just require one sha.

(defn create-commit-comment
  [user repo sha path position body options]
  (api-call :post "repos/%s/%s/commits/%s/comments" [user repo sha]
            (assoc options
              :body body
              :commit-id sha
              :path path
              :position position)))

Get a specific commit comment.

(defn specific-commit-comment
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/comments/%s" [user repo id] options))

Update a commit comment.

(defn update-commit-comment
  [user repo id body options]
  (api-call :post "repos/%s/%s/comments/%s" [user repo id] (assoc options :body body)))
(defn compare-commits
  [user repo base head & [options]]
  (api-call :get "repos/%s/%s/compare/%s...%s" [user repo base head] options))
(defn delete-commit-comment
  [user repo id options]
  (no-content? (api-call :delete "repos/%s/%s/comments/%s" [user repo id] options)))

Repo Downloads API

List the downloads for a repository.

(defn downloads
  [user repo & [options]]
  (api-call :get "repos/%s/%s/downloads" [user repo] options))

Get a specific download.

(defn specific-download
  [user repo id & [options]]
  (api-call :get "repos/%s/%s/downloads/%s" [user repo id] options))

Delete a download

(defn delete-download
  [user repo id options]
  (no-content? (api-call :delete "repos/%s/%s/downloads/%s" [user repo id] options)))

Get a download resource for a file you want to upload. You can pass it to upload-file to actually upload your file.

Github uploads are a two step process. First we get a download resource and then we use that to upload the file.

(defn download-resource
  [user repo path options]
  (let [path (file path)]
    (assoc (api-call :post "repos/%s/%s/downloads"
                     [user repo]
                     (assoc options
                       :name (.getName path)
                       :size (.length path)))
      :filepath path)))

Upload a file given a download resource obtained from download-resource.

This isn't really even a Github API call, since it calls an Amazon API. As such, it doesn't provide the same guarentees as the rest of the API. We'll just return the raw response.

(defn upload-file
  [resp]
  (post (:s3_url resp)
        {:multipart [["key" (:path resp)]
                     ["acl" (:acl resp)]
                     ["success_action_status" "201"]
                     ["Filename" (:name resp)]
                     ["AWSAccessKeyId" (:accesskeyid resp)]
                     ["Policy" (:policy resp)]
                     ["Signature" (:signature resp)]
                     ["Content-Type" (:mime_type resp)]
                     ["file" (:filepath resp)]]}))

Repo Forks API

Get a list of a repository's forks.

(defn forks
  [user repo & [options]]
  (api-call :get "repos/%s/%s/forks" [user repo] options))

Create a new fork. Options are: org -- If present, the repo is forked to this organization.

(defn create-fork
  [user repo options]
  (api-call :post "repos/%s/%s/forks" [user repo] options))

Repo Deploy Keys API

List deploy keys for a repo.

(defn keys
  [user repo options]
  (api-call :get "repos/%s/%s/keys" [user repo] options))

Get a specific deploy key.

(defn specific-key
  [user repo id options]
  (api-call :get "repos/%s/%s/keys/%s" [user repo id] options))

Create a new deploy key.

(defn create-key
  [user repo title key options]
  (api-call :post "repos/%s/%s/keys" [user repo]
            (assoc options :title title :key key)))

Edit a deploy key. Options are: title -- New title. key -- New key.

(defn edit-key
  [user repo id options]
  (api-call :post "repos/%s/%s/keys/%s" [user repo id] options))

Delete a deploy key.

(defn delete-key
  [user repo id options]
  (api-call :delete "repos/%s/%s/keys/%s" [user repo id] options))

Repo Watcher API

List a repository's watchers.

(defn watchers
  [user repo & [options]]
  (api-call :get "repos/%s/%s/watchers" [user repo] options))

List all the repositories that a user is watching.

(defn watching
  [user & [options]]
  (api-call :get "users/%s/watched" [user] options))

Check if you are watching a repository.

(defn watching?
  [user repo options]
  (no-content? (api-call :get "user/watched/%s/%s" [user repo] options)))

Watch a repository.

(defn watch
  [user repo options]
  (no-content? (api-call :put "user/watched/%s/%s" [user repo] options)))

Unwatch a repository.

(defn unwatch
  [user repo options]
  (no-content? (api-call :delete "user/watched/%s/%s" [user repo] options)))

Repo Hooks API

List the hooks on a repository.

(defn hooks
  [user repo options]
  (api-call :get "repos/%s/%s/hooks" [user repo] options))

Get a specific hook.

(defn specific-hook
  [user repo id options]
  (api-call :get "repos/%s/%s/hooks/%s" [user repo id] options))

Create a hook. Options are: events -- A sequence of event strings. Only 'push' by default. active -- true or false; determines if the hook is actually triggered on pushes.

(defn create-hook
  [user repo name config options]
  (api-call :post "repos/%s/%s/hooks" [user repo name config]
            (assoc options
              :name name, :config config)))

Edit an existing hook. Options are: name -- Name of the hook. config -- Modified config. events -- A sequence of event strings. Replaces the events. add_events -- A sequence of event strings to be added. remove_events -- A sequence of event strings to remove. active -- true or false; determines if the hook is actually triggered on pushes.

(defn edit-hook
  [user repo id options]
  (api-call :post "repos/%s/%s/hooks/%s" [user repo id] options))

Test a hook.

(defn test-hook
  [user repo id options]
  (no-content? (api-call :post "repos/%s/%s/hooks/%s/test" [user repo id] options)))

Delete a hook.

(defn delete-hook
  [user repo id options]
  (no-content? (api-call :delete "repos/%s/%s/hooks/%s" [user repo id] options)))

PubSubHubbub

Create or modify a pubsubhubub subscription. Options are: secret -- A shared secret key that generates an SHA HMAC of the payload content.

(defn pubsubhubub
  [user repo mode event callback & [options]]
  (no-content?
   (post "https://api.github.com/hub"
         {:basic-auth (:auth options)
          :form-params
          (merge
           {"hub.mode" mode
            "hub.topic" (format "https://github.com/%s/%s/events/%s"
                                user repo event)
            "hub.callback" callback}
           (when-let [secret (:secret options)]
             {"hub.secret" secret}))})))
 

Implement the Github Users API: http://developer.github.com/v3/users/

(ns tentacles.users
  (:refer-clojure :exclude [keys])
  (:use [tentacles.core :only [api-call no-content?]]))

Primary API

(defn user
  "Get info about a user."
  [user]
  (api-call :get "users/%s" [user] nil))
(defn me
  "Get info about the currently authenticated user."
  [options]
  (api-call :get "user" nil options))
(defn edit-user
  "Edit the currently authenticated user.
   Options are:
      name     -- User's name.
      email    -- User's email.
      blog     -- Link to user's blog.
      location -- User's location.
      hireable -- Looking for a job?
      bio      -- User's biography."
  [options]
  (api-call :post "user" nil options))

User Email API

(defn emails
  "List the authenticated user's emails."
  [options]
  (api-call :get "user/emails" nil options))
(defn add-emails
  "Add email address(es) to the authenticated user. emails is either
   a string or a sequence of emails addresses."
  [emails options]
  (api-call :post "user/emails" nil (assoc options :raw emails)))
(defn delete-emails
  "Delete email address(es) from the authenticated user. Emails is either
   a string or a sequence of email addresses."
  [emails options]
  (no-content? (api-call :delete "user/emails" nil (assoc options :raw emails))))

User Followers API

(defn followers
  "List a user's followers."
  [user & [options]]
  (api-call :get "users/%s/followers" [user] options))
(defn my-followers
  "List the authenticated user's followers."
  [options]
  (api-call :get "user/followers" nil options))
(defn following
  "List the users a user is following."
  [user & [options]]
  (api-call :get "users/%s/following" [user] options))
(defn my-following
  "List the users the authenticated user is following."
  [options]
  (api-call :get "user/following" nil options))
(defn following?
  "Check if the authenticated user is following another user."
  [user options]
  (no-content? (api-call :get "user/following/%s" [user] options)))
(defn follow
  "Follow a user."
  [user options]
  (no-content? (api-call :put "user/following/%s" [user] options)))
(defn unfollow
  "Unfollow a user."
  [user options]
  (no-content? (api-call :delete "user/following/%s" [user] options)))

User Keys API

(defn keys
  "List the authenticated user's public keys."
  [options]
  (api-call :get "user/keys" nil options))
(defn specific-key
  "Get a specific key from the authenticated user."
  [id options]
  (api-call :get "user/keys/%s" [id] options))
(defn create-key
  "Create a new public key."
  [title key options]
  (api-call :post "user/keys" nil (assoc options :title title :key key)))
(defn edit-key
  "Edit an existing public key.
   Options are:
      title -- New title.
      key   -- New key."
  [id options]
  (api-call :post "user/keys/%s" [id] options))
(defn delete-key
  "Delete a public key."
  [id options]
  (no-content? (api-call :delete "user/keys/%s" [id] options)))