Commit 5b2b4d48 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ee-41967_issue_api_closed_by_info' into 'master'

[port https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042] Add closed by information to issue API

See merge request gitlab-org/gitlab-ee!4855
parents 94454101 64c784d2
...@@ -27,6 +27,7 @@ class Issue < ActiveRecord::Base ...@@ -27,6 +27,7 @@ class Issue < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :moved_to, class_name: 'Issue' belongs_to :moved_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) } has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
...@@ -85,6 +86,11 @@ class Issue < ActiveRecord::Base ...@@ -85,6 +86,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue| before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now issue.closed_at = Time.zone.now
end end
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
end
end end
class << self class << self
......
...@@ -23,6 +23,7 @@ module Issues ...@@ -23,6 +23,7 @@ module Issues
end end
if project.issues_enabled? && issue.close if project.issues_enabled? && issue.close
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications notification_service.close_issue(issue, current_user) if notifications
......
---
title: adds closed by informations in issue api
merge_request: 17042
author: haseebeqx
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddClosedByToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
add_column :issues, :closed_by_id, :integer
add_concurrent_foreign_key :issues, :users, column: :closed_by_id, on_delete: :nullify
end
def down
remove_foreign_key :issues, column: :closed_by_id
remove_column :issues, :closed_by_id
end
end
...@@ -1304,6 +1304,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do ...@@ -1304,6 +1304,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.integer "last_edited_by_id" t.integer "last_edited_by_id"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at" t.datetime_with_timezone "closed_at"
t.integer "closed_by_id"
end end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
...@@ -2707,6 +2708,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do ...@@ -2707,6 +2708,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
...@@ -100,6 +100,7 @@ Example response: ...@@ -100,6 +100,7 @@ Example response:
}, },
"updated_at" : "2016-01-04T15:31:51.081Z", "updated_at" : "2016-01-04T15:31:51.081Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"id" : 76, "id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z", "created_at" : "2016-01-04T15:31:51.081Z",
...@@ -123,6 +124,8 @@ Example response: ...@@ -123,6 +124,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## List group issues ## List group issues
Get a list of a group's issues. Get a list of a group's issues.
...@@ -217,6 +220,7 @@ Example response: ...@@ -217,6 +220,7 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -235,6 +239,8 @@ Example response: ...@@ -235,6 +239,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## List project issues ## List project issues
Get a list of a project's issues. Get a list of a project's issues.
...@@ -328,6 +334,14 @@ Example response: ...@@ -328,6 +334,14 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15:31:46.176Z", "closed_at" : "2016-01-05T15:31:46.176Z",
"closed_by" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"user_notes_count": 1, "user_notes_count": 1,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -346,6 +360,8 @@ Example response: ...@@ -346,6 +360,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Single issue ## Single issue
Get a single project issue. Get a single project issue.
...@@ -412,6 +428,8 @@ Example response: ...@@ -412,6 +428,8 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"subscribed": false, "subscribed": false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
...@@ -436,6 +454,8 @@ Example response: ...@@ -436,6 +454,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## New issue ## New issue
Creates a new project issue. Creates a new project issue.
...@@ -489,6 +509,7 @@ Example response: ...@@ -489,6 +509,7 @@ Example response:
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z", "updated_at" : "2016-01-07T12:44:33.959Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"milestone" : null, "milestone" : null,
"subscribed" : true, "subscribed" : true,
"user_notes_count": 0, "user_notes_count": 0,
...@@ -514,6 +535,8 @@ Example response: ...@@ -514,6 +535,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Edit issue ## Edit issue
Updates an existing project issue. This call is also used to mark an issue as Updates an existing project issue. This call is also used to mark an issue as
...@@ -562,6 +585,14 @@ Example response: ...@@ -562,6 +585,14 @@ Example response:
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z", "updated_at" : "2016-01-07T12:55:16.213Z",
"closed_at" : "2016-01-08T12:55:16.213Z", "closed_at" : "2016-01-08T12:55:16.213Z",
"closed_by" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"iid" : 15, "iid" : 15,
"labels" : [ "labels" : [
"bug" "bug"
...@@ -594,6 +625,8 @@ Example response: ...@@ -594,6 +625,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Delete an issue ## Delete an issue
Only for admins and project owners. Soft deletes the issue in question. Only for admins and project owners. Soft deletes the issue in question.
...@@ -647,6 +680,7 @@ Example response: ...@@ -647,6 +680,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null, "closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
...@@ -695,6 +729,8 @@ Example response: ...@@ -695,6 +729,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Subscribe to an issue ## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications. Subscribes the authenticated user to an issue to receive notifications.
...@@ -727,6 +763,7 @@ Example response: ...@@ -727,6 +763,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null, "closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
...@@ -775,6 +812,9 @@ Example response: ...@@ -775,6 +812,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Unsubscribe from an issue ## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications Unsubscribes the authenticated user from the issue to not receive notifications
...@@ -816,6 +856,8 @@ Example response: ...@@ -816,6 +856,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/keyon" "web_url": "https://gitlab.example.com/keyon"
}, },
"closed_at": null,
"closed_by": null,
"author": { "author": {
"name": "Vivian Hermann", "name": "Vivian Hermann",
"username": "orville", "username": "orville",
...@@ -937,6 +979,9 @@ Example response: ...@@ -937,6 +979,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Set a time estimate for an issue ## Set a time estimate for an issue
Sets an estimated time of work for this issue. Sets an estimated time of work for this issue.
...@@ -1122,6 +1167,8 @@ Example response: ...@@ -1122,6 +1167,8 @@ Example response:
"assignee": null, "assignee": null,
"source_project_id": 1, "source_project_id": 1,
"target_project_id": 1, "target_project_id": 1,
"closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"work_in_progress": false, "work_in_progress": false,
"milestone": null, "milestone": null,
...@@ -1216,3 +1263,4 @@ Example response: ...@@ -1216,3 +1263,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004 [ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016 [ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
...@@ -82,10 +82,33 @@ class Burndown ...@@ -82,10 +82,33 @@ class Burndown
def milestone_issues def milestone_issues
@milestone_issues ||= @milestone_issues ||=
@milestone.issues begin
.where("state = 'closed' OR (state = 'opened' AND closed_at IS NOT NULL)") # We make use of `events` table to get the closed_at timestamp.
.reorder("closed_at ASC") # `issues.closed_at` can't be used once it's nullified if the issue is
.pluck("closed_at, weight, state") # reopened.
.map {|attrs| Issue.new(*attrs) } internal_clause =
::Issue
.joins("LEFT OUTER JOIN events e ON issues.id = e.target_id AND e.target_type = 'Issue'")
.where(milestone: @milestone)
.where("state = 'closed' OR (state = 'opened' AND e.action = #{Event::CLOSED})") # rubocop:disable GitlabSecurity/SqlInjection
rel =
if Gitlab::Database.postgresql?
::Issue
.select("*")
.from(internal_clause.select('DISTINCT ON (issues.id) issues.id, issues.state, issues.weight, e.created_at AS closed_at'))
else
::Issue
.select("*")
.from(internal_clause.select('issues.id, issues.state, issues.weight, e.created_at AS closed_at'))
.group('id')
.having('closed_at = MIN(closed_at) OR closed_at IS NULL')
end
rel
.order('closed_at ASC')
.pluck('closed_at, weight, state')
.map { |attrs| Issue.new(*attrs) }
end
end end
end end
...@@ -13,6 +13,10 @@ describe 'Milestones on EE' do ...@@ -13,6 +13,10 @@ describe 'Milestones on EE' do
visit project_milestone_path(project, milestone) visit project_milestone_path(project, milestone)
end end
def close_issue(issue)
Issues::CloseService.new(issue.project, user, {}).execute(issue)
end
context 'burndown charts' do context 'burndown charts' do
let(:milestone) do let(:milestone) do
create(:milestone, create(:milestone,
...@@ -36,11 +40,13 @@ describe 'Milestones on EE' do ...@@ -36,11 +40,13 @@ describe 'Milestones on EE' do
end end
end end
context 'when any closed issues do not have closed_at value' do context 'when a closed issue do not have closed events' do
it 'shows warning' do it 'shows warning' do
create(:issue, issue_params) close_issue(create(:issue, issue_params))
close_issue(create(:issue, issue_params))
# Legacy issue: No closed events being created
create(:closed_issue, issue_params) create(:closed_issue, issue_params)
create(:closed_issue, issue_params.merge(closed_at: nil))
visit_milestone visit_milestone
...@@ -50,10 +56,10 @@ describe 'Milestones on EE' do ...@@ -50,10 +56,10 @@ describe 'Milestones on EE' do
end end
end end
context 'when all closed issues do not have closed_at value' do context 'when all closed issues do not have closed events' do
it 'shows warning and hides burndown' do it 'shows warning and hides burndown' do
create(:closed_issue, issue_params.merge(closed_at: nil)) create(:closed_issue, issue_params)
create(:closed_issue, issue_params.merge(closed_at: nil)) create(:closed_issue, issue_params)
visit_milestone visit_milestone
...@@ -66,7 +72,7 @@ describe 'Milestones on EE' do ...@@ -66,7 +72,7 @@ describe 'Milestones on EE' do
context 'data is accurate' do context 'data is accurate' do
it 'does not show warning' do it 'does not show warning' do
create(:issue, issue_params) create(:issue, issue_params)
create(:closed_issue, issue_params) close_issue(create(:issue, issue_params))
visit_milestone visit_milestone
......
...@@ -65,9 +65,9 @@ describe Burndown do ...@@ -65,9 +65,9 @@ describe Burndown do
expect(burndown).to be_accurate expect(burndown).to be_accurate
end end
context "when all closed issues does not have closed_at" do context "when all closed issues does not have closed events" do
before do before do
milestone.issues.update_all(closed_at: nil) Event.where(target: milestone.issues, action: Event::CLOSED).destroy_all
end end
it "considers closed_at as milestone start date" do it "considers closed_at as milestone start date" do
...@@ -87,9 +87,9 @@ describe Burndown do ...@@ -87,9 +87,9 @@ describe Burndown do
end end
end end
context "when one or more closed issues does not have closed_at" do context "when one or more closed issues does not have a closed event" do
before do before do
milestone.issues.closed.first.update(closed_at: nil) Event.where(target: milestone.issues.closed.first, action: Event::CLOSED).destroy_all
end end
it "sets attribute accurate to false" do it "sets attribute accurate to false" do
...@@ -112,11 +112,25 @@ describe Burndown do ...@@ -112,11 +112,25 @@ describe Burndown do
# Close issues # Close issues
closed = issues.slice(0..count / 2) closed = issues.slice(0..count / 2)
closed.each(&:close) closed.each { |issue| close_issue(issue) }
# Reopen issues # Reopen issues
closed.slice(0..count / 4).each(&:reopen) reopened_issues = closed.slice(0..count / 4)
reopened_issues.each { |issue| reopen_issue(issue) }
# This generates an issue with multiple closing events
issue_closed_twice = reopened_issues.last
close_issue(issue_closed_twice)
reopen_issue(issue_closed_twice)
end end
end end
end end
def close_issue(issue)
Issues::CloseService.new(issue.project, user, {}).execute(issue)
end
def reopen_issue(issue)
Issues::ReopenService.new(issue.project, user, {}).execute(issue)
end
end end
...@@ -407,6 +407,7 @@ module API ...@@ -407,6 +407,7 @@ module API
class IssueBasic < ProjectEntity class IssueBasic < ProjectEntity
expose :closed_at expose :closed_at
expose :closed_by, using: Entities::UserBasic
expose :labels do |issue, options| expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded # Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort issue.labels.map(&:title).sort
......
...@@ -19,7 +19,7 @@ module Gitlab ...@@ -19,7 +19,7 @@ module Gitlab
custom_attributes: 'ProjectCustomAttribute', custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge' }.freeze project_badges: 'Badge' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
......
...@@ -20,6 +20,7 @@ issues: ...@@ -20,6 +20,7 @@ issues:
- issue_assignees - issue_assignees
- epic_issue - epic_issue
- epic - epic
- closed_by
events: events:
- author - author
- project - project
......
...@@ -15,6 +15,7 @@ Issue: ...@@ -15,6 +15,7 @@ Issue:
- updated_by_id - updated_by_id
- confidential - confidential
- closed_at - closed_at
- closed_by_id
- due_date - due_date
- moved_to_id - moved_to_id
- lock_version - lock_version
......
...@@ -67,6 +67,10 @@ describe Issues::CloseService do ...@@ -67,6 +67,10 @@ describe Issues::CloseService do
expect(issue).to be_closed expect(issue).to be_closed
end end
it 'records closed user' do
expect(issue.closed_by_id).to be(user.id)
end
it 'sends email to user2 about assign of new issue' do it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email) expect(email.to.first).to eq(user2.email)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment