diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml
index 4964c1bbd879888b5c930c9bef2a1252088379af..e184df549de314c696a86878d534227267aa0cfa 100644
--- a/app/views/help/api.html.haml
+++ b/app/views/help/api.html.haml
@@ -1,16 +1,18 @@
%h3 API
.back_link
- = link_to help_path do
+ = link_to help_path do
← to index
%hr
%ol
- %li
+ %li
%a{:href => "#README"} README
- %li
+ %li
%a{:href => "#projects"} Projects
- %li
+ %li
%a{:href => "#users"} Users
+ %li
+ %a{:href => "#issues"} Issues
.file_holder#README
.file_title
@@ -39,3 +41,13 @@
.file_content.wiki
= preserve do
= markdown File.read(Rails.root.join("doc", "api", "users.md"))
+
+%br
+
+.file_holder#issues
+ .file_title
+ %i.icon-file
+ Issues
+ .file_content.wiki
+ = preserve do
+ = markdown File.read(Rails.root.join("doc", "api", "issues.md"))
diff --git a/doc/api/README.md b/doc/api/README.md
index dcf75afda1f8bbdddfe79744174431efec12f025..e01119661f08c2343429ebca9e09c7dff8ff1e26 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -27,3 +27,4 @@ The API uses JSON to serialize data. You don't need to specify `.json` at the en
+ [Users](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/users.md)
+ [Projects](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md)
++ [Issues](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/issues.md)
diff --git a/doc/api/issues.md b/doc/api/issues.md
new file mode 100644
index 0000000000000000000000000000000000000000..bad2d4740202a5e77461b8c163b06a3494055b38
--- /dev/null
+++ b/doc/api/issues.md
@@ -0,0 +1,181 @@
+## List issues
+
+Get all issues created by authenticed user.
+
+```
+GET /issues
+```
+
+```json
+[
+ {
+ "id": 43,
+ "title": "4xx/5xx pages",
+ "description": "",
+ "labels": [ ],
+ "milestone": null,
+ "assignee": null,
+ "author": {
+ "id": 1,
+ "email": "john@example.com",
+ "name": "John Smith",
+ "blocked": false,
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "closed": true,
+ "updated_at": "2012-07-02T17:53:12Z",
+ "created_at": "2012-07-02T17:53:12Z"
+ },
+ {
+ "id": 42,
+ "title": "Add user settings",
+ "description": "",
+ "labels": [
+ "feature"
+ ],
+ "milestone": {
+ "id": 1,
+ "title": "v1.0",
+ "description": "",
+ "due_date": "2012-07-20",
+ "closed": false,
+ "updated_at": "2012-07-04T13:42:48Z",
+ "created_at": "2012-07-04T13:42:48Z"
+ },
+ "assignee": {
+ "id": 2,
+ "email": "jack@example.com",
+ "name": "Jack Smith",
+ "blocked": false,
+ "created_at": "2012-05-23T08:01:01Z"
+ },
+ "author": {
+ "id": 1,
+ "email": "john@example.com",
+ "name": "John Smith",
+ "blocked": false,
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "closed": false,
+ "updated_at": "2012-07-12T13:43:19Z",
+ "created_at": "2012-06-28T12:58:06Z"
+ }
+]
+```
+
+## List project issues
+
+Get a list of project issues.
+
+```
+GET /projects/:id/issues
+```
+
+Parameters:
+
++ `id` (required) - The code name of a project
+
+## Single issue
+
+Get a project issue.
+
+```
+GET /projects/:id/issues/:issue_id
+```
+
+Parameters:
+
++ `id` (required) - The code name of a project
++ `issue_id` (required) - The ID of a project issue
+
+```json
+{
+ "id": 42,
+ "title": "Add user settings",
+ "description": "",
+ "labels": [
+ "feature"
+ ],
+ "milestone": {
+ "id": 1,
+ "title": "v1.0",
+ "description": "",
+ "due_date": "2012-07-20",
+ "closed": false,
+ "updated_at": "2012-07-04T13:42:48Z",
+ "created_at": "2012-07-04T13:42:48Z"
+ },
+ "assignee": {
+ "id": 2,
+ "email": "jack@example.com",
+ "name": "Jack Smith",
+ "blocked": false,
+ "created_at": "2012-05-23T08:01:01Z"
+ },
+ "author": {
+ "id": 1,
+ "email": "john@example.com",
+ "name": "John Smith",
+ "blocked": false,
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "closed": false,
+ "updated_at": "2012-07-12T13:43:19Z",
+ "created_at": "2012-06-28T12:58:06Z"
+}
+```
+
+## New issue
+
+Create a new project issue.
+
+```
+POST /projects/:id/issues
+```
+
+Parameters:
+
++ `id` (required) - The code name of a project
++ `title` (required) - The title of an issue
++ `description` (optional) - The description of an issue
++ `assignee_id` (optional) - The ID of a user to assign issue
++ `milestone_id` (optional) - The ID of a milestone to assign issue
++ `labels` (optional) - Comma-separated label names for an issue
+
+Will return created issue with status `201 Created` on success, or `404 Not found` on fail.
+
+## Edit issue
+
+Update an existing project issue.
+
+```
+PUT /projects/:id/issues/:issue_id
+```
+
+Parameters:
+
++ `id` (required) - The code name of a project
++ `issue_id` (required) - The ID of a project's issue
++ `title` (optional) - The title of an issue
++ `description` (optional) - The description of an issue
++ `assignee_id` (optional) - The ID of a user to assign issue
++ `milestone_id` (optional) - The ID of a milestone to assign issue
++ `labels` (optional) - Comma-separated label names for an issue
++ `closed` (optional) - The state of an issue (0 = false, 1 = true)
+
+Will return updated issue with status `200 OK` on success, or `404 Not found` on fail.
+
+## Delete issue
+
+Delete existing project issue.
+
+```
+DELETE /projects/:id/issues/:issue_id
+```
+
+Parameters:
+
++ `id` (required) - The code name of a project
++ `issue_id` (required) - The ID of a project's issue
+
+Status code `200` will be returned on success.
diff --git a/lib/api.rb b/lib/api.rb
index e24e0a78f711021808973619c471736a2ed7bafb..6a8a3d651c0c209d17bf42aaee9c231842f88118 100644
--- a/lib/api.rb
+++ b/lib/api.rb
@@ -15,5 +15,6 @@ module Gitlab
mount Users
mount Projects
+ mount Issues
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 35ad4d430adfb40e5bf65aee8f1e7ee0662d7051..e5095f7189e4c284662fa991a235b7952dfc7d23 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -16,11 +16,7 @@ module Gitlab
expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at
end
- class ProjectRepositoryBranches < Grape::Entity
- expose :name, :commit
- end
-
- class ProjectRepositoryTags < Grape::Entity
+ class RepoObject < Grape::Entity
expose :name, :commit
end
@@ -29,5 +25,17 @@ module Gitlab
expose :author, :using => Entities::UserBasic
expose :expires_at, :updated_at, :created_at
end
+
+ class Milestone < Grape::Entity
+ expose :id, :title, :description, :due_date, :closed, :updated_at, :created_at
+ end
+
+ class Issue < Grape::Entity
+ expose :id, :title, :description
+ expose :label_list, :as => :labels
+ expose :milestone, :using => Entities::Milestone
+ expose :assignee, :author, :using => Entities::UserBasic
+ expose :closed, :updated_at, :created_at
+ end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 424a17b283c573039a1c617000c635ba0064a0ea..e7b9a417b0865a2e9449e65452ac675060d1a0f3 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -4,6 +4,10 @@ module Gitlab
@current_user ||= User.find_by_authentication_token(params[:private_token])
end
+ def user_project
+ @project ||= current_user.projects.find_by_code(params[:id])
+ end
+
def authenticate!
error!({'message' => '401 Unauthorized'}, 401) unless current_user
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b6a1ac269d70fc7c42fbd50241f4c5a24d29a43
--- /dev/null
+++ b/lib/api/issues.rb
@@ -0,0 +1,111 @@
+module Gitlab
+ # Issues API
+ class Issues < Grape::API
+ before { authenticate! }
+
+ resource :issues do
+ # Get currently authenticated user's issues
+ #
+ # Example Request:
+ # GET /issues
+ get do
+ present current_user.issues, :with => Entities::Issue
+ end
+ end
+
+ resource :projects do
+ # Get a list of project issues
+ #
+ # Parameters:
+ # id (required) - The code name of a project
+ # Example Request:
+ # GET /projects/:id/issues
+ get ":id/issues" do
+ present user_project.issues, :with => Entities::Issue
+ end
+
+ # Get a single project issue
+ #
+ # Parameters:
+ # id (required) - The code name of a project
+ # issue_id (required) - The ID of a project issue
+ # Example Request:
+ # GET /projects/:id/issues/:issue_id
+ get ":id/issues/:issue_id" do
+ @issue = user_project.issues.find(params[:issue_id])
+ present @issue, :with => Entities::Issue
+ end
+
+ # Create a new project issue
+ #
+ # Parameters:
+ # id (required) - The code name of a project
+ # title (required) - The title of an issue
+ # description (optional) - The description of an issue
+ # assignee_id (optional) - The ID of a user to assign issue
+ # milestone_id (optional) - The ID of a milestone to assign issue
+ # labels (optional) - The labels of an issue
+ # Example Request:
+ # POST /projects/:id/issues
+ post ":id/issues" do
+ @issue = user_project.issues.new(
+ :title => params[:title],
+ :description => params[:description],
+ :assignee_id => params[:assignee_id],
+ :milestone_id => params[:milestone_id],
+ :label_list => params[:labels]
+ )
+ @issue.author = current_user
+
+ if @issue.save
+ present @issue, :with => Entities::Issue
+ else
+ error!({'message' => '404 Not found'}, 404)
+ end
+ end
+
+ # Update an existing issue
+ #
+ # Parameters:
+ # id (required) - The code name of a project
+ # issue_id (required) - The ID of a project issue
+ # title (optional) - The title of an issue
+ # description (optional) - The description of an issue
+ # assignee_id (optional) - The ID of a user to assign issue
+ # milestone_id (optional) - The ID of a milestone to assign issue
+ # labels (optional) - The labels of an issue
+ # closed (optional) - The state of an issue (0 = false, 1 = true)
+ # Example Request:
+ # PUT /projects/:id/issues/:issue_id
+ put ":id/issues/:issue_id" do
+ @issue = user_project.issues.find(params[:issue_id])
+ parameters = {
+ :title => (params[:title] || @issue.title),
+ :description => (params[:description] || @issue.description),
+ :assignee_id => (params[:assignee_id] || @issue.assignee_id),
+ :milestone_id => (params[:milestone_id] || @issue.milestone_id),
+ :label_list => (params[:labels] || @issue.label_list),
+ :closed => (params[:closed] || @issue.closed)
+ }
+
+ if @issue.update_attributes(parameters)
+ present @issue, :with => Entities::Issue
+ else
+ error!({'message' => '404 Not found'}, 404)
+ end
+ end
+
+ # Delete a project issue
+ #
+ # Parameters:
+ # id (required) - The code name of a project
+ # issue_id (required) - The ID of a project issue
+ # Example Request:
+ # DELETE /projects/:id/issues/:issue_id
+ delete ":id/issues/:issue_id" do
+ @issue = user_project.issues.find(params[:issue_id])
+ @issue.destroy
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 70f8fa2aa62002674ae7ce4db8eabf4a07bb6a9b..34d1c23676e363c49c3bcfc083cdb4a97a70c013 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -20,8 +20,7 @@ module Gitlab
# Example Request:
# GET /projects/:id
get ":id" do
- @project = current_user.projects.find_by_code(params[:id])
- present @project, :with => Entities::Project
+ present user_project, :with => Entities::Project
end
# Get a project repository branches
@@ -31,8 +30,7 @@ module Gitlab
# Example Request:
# GET /projects/:id/repository/branches
get ":id/repository/branches" do
- @project = current_user.projects.find_by_code(params[:id])
- present @project.repo.heads.sort_by(&:name), :with => Entities::ProjectRepositoryBranches
+ present user_project.repo.heads.sort_by(&:name), :with => Entities::RepoObject
end
# Get a project repository tags
@@ -42,8 +40,7 @@ module Gitlab
# Example Request:
# GET /projects/:id/repository/tags
get ":id/repository/tags" do
- @project = current_user.projects.find_by_code(params[:id])
- present @project.repo.tags.sort_by(&:name).reverse, :with => Entities::ProjectRepositoryTags
+ present user_project.repo.tags.sort_by(&:name).reverse, :with => Entities::RepoObject
end
# Get a project snippet
@@ -54,8 +51,7 @@ module Gitlab
# Example Request:
# GET /projects/:id/snippets/:snippet_id
get ":id/snippets/:snippet_id" do
- @project = current_user.projects.find_by_code(params[:id])
- @snippet = @project.snippets.find(params[:snippet_id])
+ @snippet = user_project.snippets.find(params[:snippet_id])
present @snippet, :with => Entities::ProjectSnippet
end
@@ -70,8 +66,7 @@ module Gitlab
# Example Request:
# POST /projects/:id/snippets
post ":id/snippets" do
- @project = current_user.projects.find_by_code(params[:id])
- @snippet = @project.snippets.new(
+ @snippet = user_project.snippets.new(
:title => params[:title],
:file_name => params[:file_name],
:expires_at => params[:lifetime],
@@ -98,8 +93,7 @@ module Gitlab
# Example Request:
# PUT /projects/:id/snippets/:snippet_id
put ":id/snippets/:snippet_id" do
- @project = current_user.projects.find_by_code(params[:id])
- @snippet = @project.snippets.find(params[:snippet_id])
+ @snippet = user_project.snippets.find(params[:snippet_id])
parameters = {
:title => (params[:title] || @snippet.title),
:file_name => (params[:file_name] || @snippet.file_name),
@@ -122,8 +116,7 @@ module Gitlab
# Example Request:
# DELETE /projects/:id/snippets/:snippet_id
delete ":id/snippets/:snippet_id" do
- @project = current_user.projects.find_by_code(params[:id])
- @snippet = @project.snippets.find(params[:snippet_id])
+ @snippet = user_project.snippets.find(params[:snippet_id])
@snippet.destroy
end
@@ -135,8 +128,7 @@ module Gitlab
# Example Request:
# GET /projects/:id/snippets/:snippet_id/raw
get ":id/snippets/:snippet_id/raw" do
- @project = current_user.projects.find_by_code(params[:id])
- @snippet = @project.snippets.find(params[:snippet_id])
+ @snippet = user_project.snippets.find(params[:snippet_id])
present @snippet.content
end
end
diff --git a/spec/api/issues_spec.rb b/spec/api/issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d81a07e214b84e94a679b7319a84aff89f13f1ce
--- /dev/null
+++ b/spec/api/issues_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Gitlab::API do
+ let(:user) { Factory :user }
+ let!(:project) { Factory :project, :owner => user }
+ let!(:issue) { Factory :issue, :author => user, :assignee => user, :project => project }
+ before { project.add_access(user, :read) }
+
+ describe "GET /issues" do
+ it "should return authentication error" do
+ get "#{api_prefix}/issues"
+ response.status.should == 401
+ end
+
+ describe "authenticated GET /issues" do
+ it "should return an array of issues" do
+ get "#{api_prefix}/issues?private_token=#{user.private_token}"
+ response.status.should == 200
+ json_response.should be_an Array
+ json_response.first['title'].should == issue.title
+ end
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ it "should return project issues" do
+ get "#{api_prefix}/projects/#{project.code}/issues?private_token=#{user.private_token}"
+ response.status.should == 200
+ json_response.should be_an Array
+ json_response.first['title'].should == issue.title
+ end
+ end
+
+ describe "GET /projects/:id/issues/:issue_id" do
+ it "should return a project issue by id" do
+ get "#{api_prefix}/projects/#{project.code}/issues/#{issue.id}?private_token=#{user.private_token}"
+ response.status.should == 200
+ json_response['title'].should == issue.title
+ end
+ end
+
+ describe "POST /projects/:id/issues" do
+ it "should create a new project issue" do
+ post "#{api_prefix}/projects/#{project.code}/issues?private_token=#{user.private_token}",
+ :title => 'new issue', :labels => 'label, label2'
+ response.status.should == 201
+ json_response['title'].should == 'new issue'
+ json_response['description'].should be_nil
+ json_response['labels'].should == ['label', 'label2']
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id" do
+ it "should update a project issue" do
+ put "#{api_prefix}/projects/#{project.code}/issues/#{issue.id}?private_token=#{user.private_token}",
+ :title => 'updated title', :labels => 'label2', :closed => 1
+ response.status.should == 200
+ json_response['title'].should == 'updated title'
+ json_response['labels'].should == ['label2']
+ json_response['closed'].should be_true
+ end
+ end
+
+ describe "DELETE /projects/:id/issues/:issue_id" do
+ it "should delete a project issue" do
+ expect {
+ delete "#{api_prefix}/projects/#{project.code}/issues/#{issue.id}?private_token=#{user.private_token}"
+ }.to change { Issue.count }.by(-1)
+ end
+ end
+end