Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
Commits
17196a2f
Commit
17196a2f
authored
Dec 23, 2016
by
Ruben Davila
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Backport backend work for time tracking.
parent
64dd41a0
Changes
30
Hide whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
692 additions
and
17 deletions
+692
-17
app/assets/javascripts/lib/vue_resource.js.es6
app/assets/javascripts/lib/vue_resource.js.es6
+2
-0
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+2
-0
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+9
-0
app/models/concerns/issuable.rb
app/models/concerns/issuable.rb
+1
-0
app/models/concerns/time_trackable.rb
app/models/concerns/time_trackable.rb
+58
-0
app/models/timelog.rb
app/models/timelog.rb
+6
-0
app/serializers/issuable_entity.rb
app/serializers/issuable_entity.rb
+4
-0
app/services/issuable_base_service.rb
app/services/issuable_base_service.rb
+25
-1
app/services/slash_commands/interpret_service.rb
app/services/slash_commands/interpret_service.rb
+47
-0
app/services/system_note_service.rb
app/services/system_note_service.rb
+51
-0
db/migrate/20161223034433_add_time_estimate_to_issuables.rb
db/migrate/20161223034433_add_time_estimate_to_issuables.rb
+30
-0
db/migrate/20161223034646_create_timelogs.rb
db/migrate/20161223034646_create_timelogs.rb
+38
-0
db/schema.rb
db/schema.rb
+14
-0
doc/user/project/slash_commands.md
doc/user/project/slash_commands.md
+4
-0
doc/workflow/README.md
doc/workflow/README.md
+1
-0
doc/workflow/time-tracking/time-tracking-example.png
doc/workflow/time-tracking/time-tracking-example.png
+0
-0
doc/workflow/time-tracking/time-tracking-sidebar.png
doc/workflow/time-tracking/time-tracking-sidebar.png
+0
-0
doc/workflow/time_tracking.md
doc/workflow/time_tracking.md
+76
-0
lib/gitlab/import_export/import_export.yml
lib/gitlab/import_export/import_export.yml
+2
-0
lib/gitlab/time_tracking_formatter.rb
lib/gitlab/time_tracking_formatter.rb
+30
-0
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+32
-16
spec/factories/timelogs.rb
spec/factories/timelogs.rb
+9
-0
spec/features/issues/user_uses_slash_commands_spec.rb
spec/features/issues/user_uses_slash_commands_spec.rb
+26
-0
spec/lib/gitlab/import_export/all_models.yml
spec/lib/gitlab/import_export/all_models.yml
+5
-0
spec/lib/gitlab/import_export/safe_model_attributes.yml
spec/lib/gitlab/import_export/safe_model_attributes.yml
+10
-0
spec/models/concerns/issuable_spec.rb
spec/models/concerns/issuable_spec.rb
+38
-0
spec/models/timelog_spec.rb
spec/models/timelog_spec.rb
+10
-0
spec/services/notes/slash_commands_service_spec.rb
spec/services/notes/slash_commands_service_spec.rb
+12
-0
spec/services/slash_commands/interpret_service_spec.rb
spec/services/slash_commands/interpret_service_spec.rb
+85
-0
spec/services/system_note_service_spec.rb
spec/services/system_note_service_spec.rb
+65
-0
No files found.
app/assets/javascripts/lib/vue_resource.js.es6
0 → 100644
View file @
17196a2f
//= require vue
//= require vue-resource
app/assets/stylesheets/framework/variables.scss
View file @
17196a2f
...
@@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3);
...
@@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light
:
darken
(
$white-light
,
$darken-border-factor
);
$border-white-light
:
darken
(
$white-light
,
$darken-border-factor
);
$border-white-normal
:
darken
(
$white-normal
,
$darken-border-factor
);
$border-white-normal
:
darken
(
$white-normal
,
$darken-border-factor
);
$border-gray-light
:
darken
(
$gray-light
,
$darken-border-factor
);
$border-gray-normal
:
darken
(
$gray-normal
,
$darken-border-factor
);
$border-gray-normal
:
darken
(
$gray-normal
,
$darken-border-factor
);
$border-gray-dark
:
darken
(
$white-normal
,
$darken-border-factor
);
$border-gray-dark
:
darken
(
$white-normal
,
$darken-border-factor
);
...
@@ -274,6 +275,7 @@ $dropdown-hover-color: #3b86ff;
...
@@ -274,6 +275,7 @@ $dropdown-hover-color: #3b86ff;
*/
*/
$btn-active-gray
:
#ececec
;
$btn-active-gray
:
#ececec
;
$btn-active-gray-light
:
e4e7ed
;
$btn-active-gray-light
:
e4e7ed
;
$btn-white-active
:
#848484
;
/*
/*
* Badges
* Badges
...
...
app/helpers/issuables_helper.rb
View file @
17196a2f
...
@@ -30,6 +30,15 @@ module IssuablesHelper
...
@@ -30,6 +30,15 @@ module IssuablesHelper
end
end
end
end
def
serialize_issuable
(
issuable
)
case
issuable
when
Issue
IssueSerializer
.
new
.
represent
(
issuable
).
to_json
when
MergeRequest
MergeRequestSerializer
.
new
.
represent
(
issuable
).
to_json
end
end
def
template_dropdown_tag
(
issuable
,
&
block
)
def
template_dropdown_tag
(
issuable
,
&
block
)
title
=
selected_template
(
issuable
)
||
"Choose a template"
title
=
selected_template
(
issuable
)
||
"Choose a template"
options
=
{
options
=
{
...
...
app/models/concerns/issuable.rb
View file @
17196a2f
...
@@ -13,6 +13,7 @@ module Issuable
...
@@ -13,6 +13,7 @@ module Issuable
include
StripAttribute
include
StripAttribute
include
Awardable
include
Awardable
include
Taskable
include
Taskable
include
TimeTrackable
included
do
included
do
cache_markdown_field
:title
,
pipeline: :single_line
cache_markdown_field
:title
,
pipeline: :single_line
...
...
app/models/concerns/time_trackable.rb
0 → 100644
View file @
17196a2f
# == TimeTrackable concern
#
# Contains functionality related to objects that support time tracking.
#
# Used by Issue and MergeRequest.
#
module
TimeTrackable
extend
ActiveSupport
::
Concern
included
do
attr_reader
:time_spent
alias_method
:time_spent?
,
:time_spent
default_value_for
:time_estimate
,
value:
0
,
allows_nil:
false
has_many
:timelogs
,
as: :trackable
,
dependent: :destroy
end
def
spend_time
(
seconds
,
user
)
return
if
seconds
==
0
@time_spent
=
seconds
@time_spent_user
=
user
if
seconds
==
:reset
reset_spent_time
else
add_or_subtract_spent_time
end
end
def
total_time_spent
timelogs
.
sum
(
:time_spent
)
end
def
human_total_time_spent
Gitlab
::
TimeTrackingFormatter
.
output
(
total_time_spent
)
end
def
human_time_estimate
Gitlab
::
TimeTrackingFormatter
.
output
(
time_estimate
)
end
private
def
reset_spent_time
timelogs
.
new
(
time_spent:
total_time_spent
*
-
1
,
user:
@time_spent_user
)
end
def
add_or_subtract_spent_time
# Exit if time to subtract exceeds the total time spent.
return
if
time_spent
<
0
&&
(
time_spent
.
abs
>
total_time_spent
)
timelogs
.
new
(
time_spent:
time_spent
,
user:
@time_spent_user
)
end
end
app/models/timelog.rb
0 → 100644
View file @
17196a2f
class
Timelog
<
ActiveRecord
::
Base
validates
:time_spent
,
:user
,
presence:
true
belongs_to
:trackable
,
polymorphic:
true
belongs_to
:user
end
app/serializers/issuable_entity.rb
View file @
17196a2f
...
@@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity
...
@@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity
expose
:created_at
expose
:created_at
expose
:updated_at
expose
:updated_at
expose
:deleted_at
expose
:deleted_at
expose
:time_estimate
expose
:total_time_spent
expose
:human_time_estimate
expose
:human_total_time_spent
end
end
app/services/issuable_base_service.rb
View file @
17196a2f
...
@@ -36,6 +36,14 @@ class IssuableBaseService < BaseService
...
@@ -36,6 +36,14 @@ class IssuableBaseService < BaseService
end
end
end
end
def
create_time_estimate_note
(
issuable
)
SystemNoteService
.
change_time_estimate
(
issuable
,
issuable
.
project
,
current_user
)
end
def
create_time_spent_note
(
issuable
)
SystemNoteService
.
change_time_spent
(
issuable
,
issuable
.
project
,
current_user
)
end
def
filter_params
(
issuable
)
def
filter_params
(
issuable
)
ability_name
=
:"admin_
#{
issuable
.
to_ability_name
}
"
ability_name
=
:"admin_
#{
issuable
.
to_ability_name
}
"
...
@@ -156,6 +164,7 @@ class IssuableBaseService < BaseService
...
@@ -156,6 +164,7 @@ class IssuableBaseService < BaseService
def
create
(
issuable
)
def
create
(
issuable
)
merge_slash_commands_into_params!
(
issuable
)
merge_slash_commands_into_params!
(
issuable
)
filter_params
(
issuable
)
filter_params
(
issuable
)
change_time_spent
(
issuable
)
params
.
delete
(
:state_event
)
params
.
delete
(
:state_event
)
params
[
:author
]
||=
current_user
params
[
:author
]
||=
current_user
...
@@ -198,13 +207,14 @@ class IssuableBaseService < BaseService
...
@@ -198,13 +207,14 @@ class IssuableBaseService < BaseService
change_subscription
(
issuable
)
change_subscription
(
issuable
)
change_todo
(
issuable
)
change_todo
(
issuable
)
filter_params
(
issuable
)
filter_params
(
issuable
)
time_spent
=
change_time_spent
(
issuable
)
old_labels
=
issuable
.
labels
.
to_a
old_labels
=
issuable
.
labels
.
to_a
old_mentioned_users
=
issuable
.
mentioned_users
.
to_a
old_mentioned_users
=
issuable
.
mentioned_users
.
to_a
label_ids
=
process_label_ids
(
params
,
existing_label_ids:
issuable
.
label_ids
)
label_ids
=
process_label_ids
(
params
,
existing_label_ids:
issuable
.
label_ids
)
params
[
:label_ids
]
=
label_ids
if
labels_changing?
(
issuable
.
label_ids
,
label_ids
)
params
[
:label_ids
]
=
label_ids
if
labels_changing?
(
issuable
.
label_ids
,
label_ids
)
if
params
.
present?
&&
update_issuable
(
issuable
,
params
)
if
(
params
.
present?
||
time_spent
)
&&
update_issuable
(
issuable
,
params
)
# We do not touch as it will affect a update on updated_at field
# We do not touch as it will affect a update on updated_at field
ActiveRecord
::
Base
.
no_touching
do
ActiveRecord
::
Base
.
no_touching
do
handle_common_system_notes
(
issuable
,
old_labels:
old_labels
)
handle_common_system_notes
(
issuable
,
old_labels:
old_labels
)
...
@@ -251,6 +261,12 @@ class IssuableBaseService < BaseService
...
@@ -251,6 +261,12 @@ class IssuableBaseService < BaseService
end
end
end
end
def
change_time_spent
(
issuable
)
time_spent
=
params
.
delete
(
:spend_time
)
issuable
.
spend_time
(
time_spent
,
current_user
)
if
time_spent
end
def
has_changes?
(
issuable
,
old_labels:
[])
def
has_changes?
(
issuable
,
old_labels:
[])
valid_attrs
=
[
:title
,
:description
,
:assignee_id
,
:milestone_id
,
:target_branch
]
valid_attrs
=
[
:title
,
:description
,
:assignee_id
,
:milestone_id
,
:target_branch
]
...
@@ -272,6 +288,14 @@ class IssuableBaseService < BaseService
...
@@ -272,6 +288,14 @@ class IssuableBaseService < BaseService
create_task_status_note
(
issuable
)
create_task_status_note
(
issuable
)
end
end
if
issuable
.
previous_changes
.
include?
(
'time_estimate'
)
create_time_estimate_note
(
issuable
)
end
if
issuable
.
time_spent?
create_time_spent_note
(
issuable
)
end
create_labels_note
(
issuable
,
old_labels
)
if
issuable
.
labels
!=
old_labels
create_labels_note
(
issuable
,
old_labels
)
if
issuable
.
labels
!=
old_labels
end
end
end
end
app/services/slash_commands/interpret_service.rb
View file @
17196a2f
...
@@ -243,6 +243,53 @@ module SlashCommands
...
@@ -243,6 +243,53 @@ module SlashCommands
@updates
[
:wip_event
]
=
issuable
.
work_in_progress?
?
'unwip'
:
'wip'
@updates
[
:wip_event
]
=
issuable
.
work_in_progress?
?
'unwip'
:
'wip'
end
end
desc
'Set time estimate'
params
'<1w 3d 2h 14m>'
condition
do
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
command
:estimate
do
|
raw_duration
|
time_estimate
=
Gitlab
::
TimeTrackingFormatter
.
parse
(
raw_duration
)
if
time_estimate
@updates
[
:time_estimate
]
=
time_estimate
end
end
desc
'Add or substract spent time'
params
'<1h 30m | -1h 30m>'
condition
do
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
issuable
)
end
command
:spend
do
|
raw_duration
|
reduce_time
=
raw_duration
.
sub!
(
/\A-/
,
''
)
time_spent
=
Gitlab
::
TimeTrackingFormatter
.
parse
(
raw_duration
)
if
time_spent
time_spent
*=
-
1
if
reduce_time
@updates
[
:spend_time
]
=
time_spent
end
end
desc
'Remove time estimate'
condition
do
issuable
.
persisted?
&&
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
command
:remove_estimate
do
@updates
[
:time_estimate
]
=
0
end
desc
'Remove spent time'
condition
do
issuable
.
persisted?
&&
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
command
:remove_time_spent
do
@updates
[
:spend_time
]
=
:reset
end
# This is a dummy command, so that it appears in the autocomplete commands
# This is a dummy command, so that it appears in the autocomplete commands
desc
'CC'
desc
'CC'
params
'@user'
params
'@user'
...
...
app/services/system_note_service.rb
View file @
17196a2f
...
@@ -109,6 +109,57 @@ module SystemNoteService
...
@@ -109,6 +109,57 @@ module SystemNoteService
create_note
(
noteable:
noteable
,
project:
project
,
author:
author
,
note:
body
)
create_note
(
noteable:
noteable
,
project:
project
,
author:
author
,
note:
body
)
end
end
# Called when the estimated time of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# time_estimate - Estimated time
#
# Example Note text:
#
# "Changed estimate of this issue to 3d 5h"
#
# Returns the created Note object
def
change_time_estimate
(
noteable
,
project
,
author
)
parsed_time
=
Gitlab
::
TimeTrackingFormatter
.
output
(
noteable
.
time_estimate
)
body
=
if
noteable
.
time_estimate
==
0
"Removed time estimate on this
#{
noteable
.
human_class_name
}
"
else
"Changed time estimate of this
#{
noteable
.
human_class_name
}
to
#{
parsed_time
}
"
end
create_note
(
noteable:
noteable
,
project:
project
,
author:
author
,
note:
body
)
end
# Called when the spent time of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# time_spent - Spent time
#
# Example Note text:
#
# "Added 2h 30m of time spent on this issue"
#
# Returns the created Note object
def
change_time_spent
(
noteable
,
project
,
author
)
time_spent
=
noteable
.
time_spent
if
time_spent
==
:reset
body
=
"Removed time spent on this
#{
noteable
.
human_class_name
}
"
else
parsed_time
=
Gitlab
::
TimeTrackingFormatter
.
output
(
time_spent
.
abs
)
action
=
time_spent
>
0
?
'Added'
:
'Subtracted'
body
=
"
#{
action
}
#{
parsed_time
}
of time spent on this
#{
noteable
.
human_class_name
}
"
end
create_note
(
noteable:
noteable
,
project:
project
,
author:
author
,
note:
body
)
end
# Called when the status of a Noteable is changed
# Called when the status of a Noteable is changed
#
#
# noteable - Noteable object
# noteable - Noteable object
...
...
db/migrate/20161223034433_add_time_estimate_to_issuables.rb
0 → 100644
View file @
17196a2f
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class
AddTimeEstimateToIssuables
<
ActiveRecord
::
Migration
include
Gitlab
::
Database
::
MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME
=
false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def
change
add_column
:issues
,
:time_estimate
,
:integer
add_column
:merge_requests
,
:time_estimate
,
:integer
end
end
db/migrate/20161223034646_create_timelogs.rb
0 → 100644
View file @
17196a2f
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class
CreateTimelogs
<
ActiveRecord
::
Migration
include
Gitlab
::
Database
::
MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME
=
false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def
change
create_table
:timelogs
do
|
t
|
t
.
integer
:time_spent
,
null:
false
t
.
references
:trackable
,
polymorphic:
true
t
.
references
:user
t
.
timestamps
null:
false
end
add_index
:timelogs
,
[
:trackable_type
,
:trackable_id
]
add_index
:timelogs
,
:user_id
end
end
db/schema.rb
View file @
17196a2f
...
@@ -506,6 +506,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
...
@@ -506,6 +506,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t
.
integer
"lock_version"
t
.
integer
"lock_version"
t
.
text
"title_html"
t
.
text
"title_html"
t
.
text
"description_html"
t
.
text
"description_html"
t
.
integer
"time_estimate"
end
end
add_index
"issues"
,
[
"assignee_id"
],
name:
"index_issues_on_assignee_id"
,
using: :btree
add_index
"issues"
,
[
"assignee_id"
],
name:
"index_issues_on_assignee_id"
,
using: :btree
...
@@ -685,6 +686,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
...
@@ -685,6 +686,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t
.
integer
"lock_version"
t
.
integer
"lock_version"
t
.
text
"title_html"
t
.
text
"title_html"
t
.
text
"description_html"
t
.
text
"description_html"
t
.
integer
"time_estimate"
end
end
add_index
"merge_requests"
,
[
"assignee_id"
],
name:
"index_merge_requests_on_assignee_id"
,
using: :btree
add_index
"merge_requests"
,
[
"assignee_id"
],
name:
"index_merge_requests_on_assignee_id"
,
using: :btree
...
@@ -1128,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do
...
@@ -1128,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do
add_index
"tags"
,
[
"name"
],
name:
"index_tags_on_name"
,
unique:
true
,
using: :btree
add_index
"tags"
,
[
"name"
],
name:
"index_tags_on_name"
,
unique:
true
,
using: :btree
create_table
"timelogs"
,
force: :cascade
do
|
t
|
t
.
integer
"time_spent"
,
null:
false
t
.
integer
"trackable_id"
t
.
string
"trackable_type"
t
.
integer
"user_id"
t
.
datetime
"created_at"
,
null:
false
t
.
datetime
"updated_at"
,
null:
false
end
add_index
"timelogs"
,
[
"trackable_type"
,
"trackable_id"
],
name:
"index_timelogs_on_trackable_type_and_trackable_id"
,
using: :btree
add_index
"timelogs"
,
[
"user_id"
],
name:
"index_timelogs_on_user_id"
,
using: :btree
create_table
"todos"
,
force: :cascade
do
|
t
|
create_table
"todos"
,
force: :cascade
do
|
t
|
t
.
integer
"user_id"
,
null:
false
t
.
integer
"user_id"
,
null:
false
t
.
integer
"project_id"
,
null:
false
t
.
integer
"project_id"
,
null:
false
...
...
doc/user/project/slash_commands.md
View file @
17196a2f
...
@@ -29,3 +29,7 @@ do.
...
@@ -29,3 +29,7 @@ do.
|
<code>
/due
<
in 2 days
|
this Friday
|
December 31st
>
</code>
| Set due date |
|
<code>
/due
<
in 2 days
|
this Friday
|
December 31st
>
</code>
| Set due date |
|
`/remove_due_date`
| Remove due date |
|
`/remove_due_date`
| Remove due date |
|
`/wip`
| Toggle the Work In Progress status |
|
`/wip`
| Toggle the Work In Progress status |
|
<code>
/estimate
<
1w 3d 2h 14m
>
</code>
| Set time estimate |
|
`/remove_estimate`
| Remove estimated time |
|
<code>
/spend
<
1h 30m
|
-1h 5m
>
</code>
| Add or substract spent time |
|
`/remove_time_spent`
| Remove time spent |
doc/workflow/README.md
View file @
17196a2f
...
@@ -19,6 +19,7 @@
...
@@ -19,6 +19,7 @@
-
[
Slash commands
](
../user/project/slash_commands.md
)
-
[
Slash commands
](
../user/project/slash_commands.md
)
-
[
Sharing a project with a group
](
share_with_group.md
)
-
[
Sharing a project with a group
](
share_with_group.md
)
-
[
Share projects with other groups
](
share_projects_with_other_groups.md
)
-
[
Share projects with other groups
](
share_projects_with_other_groups.md
)
-
[
Time tracking
](
time_tracking.md
)
-
[
Web Editor
](
../user/project/repository/web_editor.md
)
-
[
Web Editor
](
../user/project/repository/web_editor.md
)
-
[
Releases
](
releases.md
)
-
[
Releases
](
releases.md
)
-
[
Milestones
](
milestones.md
)
-
[
Milestones
](
milestones.md
)
...
...
doc/workflow/time-tracking/time-tracking-example.png
0 → 100644
View file @
17196a2f
47.2 KB
doc/workflow/time-tracking/time-tracking-sidebar.png
0 → 100644
View file @
17196a2f
19 KB
doc/workflow/time_tracking.md
0 → 100644
View file @
17196a2f
# Time Tracking
> Introduced in GitLab 8.14.
Time Tracking lets teams stack their project estimates against their time spent.
Other interesting links:
-
[
Time Tracking landing page on about.gitlab.com
][
landing
]
## Overview
Time Tracking lets you:
*
record the time spent working on an issue or a merge request,
*
add an estimate of the amount of time needed to complete an issue or a merge
request.
You don't have to indicate an estimate to enter the time spent, and vice versa.
Data about time tracking is shown on the issue/merge request sidebar, as shown
below.
![
Time tracking in the sidebar
](
time-tracking/time-tracking-sidebar.png
)
## How to enter data
Time Tracking uses two [slash commands] that GitLab introduced with this new
feature:
`/spend`
and
`/estimate`
.
Slash commands can be used in the body of an issue or a merge request, but also
in a comment in both an issue or a merge request.
Below is an example of how you can use those new slash commands inside a comment.
![
Time tracking example in a comment
](
time-tracking/time-tracking-example.png
)
Adding time entries (time spent or estimates) is limited to project members.
### Estimates
To enter an estimate, write
`/estimate`
, followed by the time. For example, if
you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
`/estimate 3d 5h 10m`
.
Every time you enter a new time estimate, any previous time estimates will be
overridden by this new value. There should only be one valid estimate in an
issue or a merge request.
To remove an estimation entirely, use
`/remove_estimation`
.
### Time spent
To enter a time spent, use
`/spend 3d 5h 10m`
.
Every new time spent entry will be added to the current total time spent for the
issue or the merge request.
You can remove time by entering a negative amount:
`/spend -3d`
will remove 3
days from the total time spent. You can't go below 0 minutes of time spent,
so GitLab will automatically reset the time spent if you remove a larger amount
of time compared to the time that was entered already.
To remove all the time spent at once, use
`/remove_time_spent`
.
## Configuration
The following time units are available:
*
weeks (w)
*
days (d)
*
hours (h)
*
minutes (m)
Default conversion rates are 1w = 5d and 1d = 8h.
[
landing
]:
https://about.gitlab.com/features/time-tracking
[
slash-commands
]:
../user/project/slash_commands.md
lib/gitlab/import_export/import_export.yml
View file @
17196a2f
...
@@ -6,6 +6,7 @@ project_tree:
...
@@ -6,6 +6,7 @@ project_tree:
-
:events
-
:events
-
issues
:
-
issues
:
-
:events
-
:events
-
:timelogs
-
notes
:
-
notes
:
-
:author
-
:author
-
:events
-
:events
...
@@ -27,6 +28,7 @@ project_tree:
...
@@ -27,6 +28,7 @@ project_tree:
-
:events
-
:events
-
:merge_request_diff
-
:merge_request_diff
-
:events
-
:events
-
:timelogs
-
label_links
:
-
label_links
:
-
label
:
-
label
:
:priorities
:priorities
...
...
lib/gitlab/time_tracking_formatter.rb
0 → 100644
View file @
17196a2f
module
Gitlab
module
TimeTrackingFormatter
extend
self
def
parse
(
string
)
with_custom_config
do
ChronicDuration
.
parse
(
string
,
default_unit:
'hours'
)
rescue
nil
end
end
def
output
(
seconds
)
with_custom_config
do
ChronicDuration
.
output
(
seconds
,
format: :short
,
limit_to_hours:
false
,
weeks:
true
)
rescue
nil
end
end
def
with_custom_config
# We may want to configure it through project settings in a future version.
ChronicDuration
.
hours_per_day
=
8
ChronicDuration
.
days_per_week
=
5
result
=
yield
ChronicDuration
.
hours_per_day
=
24
ChronicDuration
.
days_per_week
=
7
result
end
end
end
spec/controllers/projects/issues_controller_spec.rb
View file @
17196a2f
...
@@ -326,6 +326,20 @@ describe Projects::IssuesController do
...
@@ -326,6 +326,20 @@ describe Projects::IssuesController do
end
end
describe
'POST #create'
do
describe
'POST #create'
do
def
post_new_issue
(
attrs
=
{})
sign_in
(
user
)
project
=
create
(
:empty_project
,
:public
)
project
.
team
<<
[
user
,
:developer
]
post
:create
,
{
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
.
to_param
,
issue:
{
title:
'Title'
,
description:
'Description'
}.
merge
(
attrs
)
}
project
.
issues
.
first
end
context
'resolving discussions in MergeRequest'
do
context
'resolving discussions in MergeRequest'
do
let
(
:discussion
)
{
Discussion
.
for_diff_notes
([
create
(
:diff_note_on_merge_request
)]).
first
}
let
(
:discussion
)
{
Discussion
.
for_diff_notes
([
create
(
:diff_note_on_merge_request
)]).
first
}
let
(
:merge_request
)
{
discussion
.
noteable
}
let
(
:merge_request
)
{
discussion
.
noteable
}
...
@@ -369,13 +383,7 @@ describe Projects::IssuesController do
...
@@ -369,13 +383,7 @@ describe Projects::IssuesController do
end
end
def
post_spam_issue
def
post_spam_issue
sign_in
(
user
)
post_new_issue
(
title:
'Spam Title'
,
description:
'Spam lives here'
)
spam_project
=
create
(
:empty_project
,
:public
)
post
:create
,
{
namespace_id:
spam_project
.
namespace
.
to_param
,
project_id:
spam_project
.
to_param
,
issue:
{
title:
'Spam Title'
,
description:
'Spam lives here'
}
}
end
end
it
'rejects an issue recognized as spam'
do
it
'rejects an issue recognized as spam'
do
...
@@ -396,18 +404,26 @@ describe Projects::IssuesController do
...
@@ -396,18 +404,26 @@ describe Projects::IssuesController do
request
.
env
[
'action_dispatch.remote_ip'
]
=
'127.0.0.1'
request
.
env
[
'action_dispatch.remote_ip'
]
=
'127.0.0.1'
end
end
def
post_new_issue
it
'creates a user agent detail'
do
expect
{
post_new_issue
}.
to
change
(
UserAgentDetail
,
:count
).
by
(
1
)
end
end
context
'when description has slash commands'
do
before
do
sign_in
(
user
)
sign_in
(
user
)
project
=
create
(
:empty_project
,
:public
)
post
:create
,
{
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
.
to_param
,
issue:
{
title:
'Title'
,
description:
'Description'
}
}
end
end
it
'creates a user agent detail'
do
it
'can add spent time'
do
expect
{
post_new_issue
}.
to
change
(
UserAgentDetail
,
:count
).
by
(
1
)
issue
=
post_new_issue
(
description:
'/spend 1h'
)
expect
(
issue
.
total_time_spent
).
to
eq
(
3600
)
end
it
'can set the time estimate'
do
issue
=
post_new_issue
(
description:
'/estimate 2h'
)
expect
(
issue
.
time_estimate
).
to
eq
(
7200
)
end
end
end
end
end
end
...
...
spec/factories/timelogs.rb
0 → 100644
View file @
17196a2f
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl
.
define
do
factory
:timelog
do
time_spent
3600
user
association
:trackable
,
factory: :issue
end
end
spec/features/issues/user_uses_slash_commands_spec.rb
View file @
17196a2f
...
@@ -126,6 +126,32 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
...
@@ -126,6 +126,32 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end
end
end
end
describe
'Issuable time tracking'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
before
do
project
.
team
<<
[
user
,
:developer
]
end
context
'Issue'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
it_behaves_like
'issuable time tracker'
end
context
'Merge Request'
do
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
)
}
before
do
visit
namespace_project_merge_request_path
(
project
.
namespace
,
project
,
merge_request
)
end
it_behaves_like
'issuable time tracker'
end
end
describe
'toggling the WIP prefix from the title from note'
do
describe
'toggling the WIP prefix from the title from note'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
...
...
spec/lib/gitlab/import_export/all_models.yml
View file @
17196a2f
...
@@ -15,6 +15,7 @@ issues:
...
@@ -15,6 +15,7 @@ issues:
-
events
-
events
-
merge_requests_closing_issues
-
merge_requests_closing_issues
-
metrics
-
metrics
-
timelogs
events
:
events
:
-
author
-
author
-
project
-
project
...
@@ -77,6 +78,7 @@ merge_requests:
...
@@ -77,6 +78,7 @@ merge_requests:
-
events
-
events
-
merge_requests_closing_issues
-
merge_requests_closing_issues
-
metrics
-
metrics
-
timelogs
merge_request_diff
:
merge_request_diff
:
-
merge_request
-
merge_request
pipelines
:
pipelines
:
...
@@ -198,3 +200,6 @@ award_emoji:
...
@@ -198,3 +200,6 @@ award_emoji:
-
user
-
user
priorities
:
priorities
:
-
label
-
label
timelogs
:
-
trackable
-
user
spec/lib/gitlab/import_export/safe_model_attributes.yml
View file @
17196a2f
...
@@ -20,6 +20,7 @@ Issue:
...
@@ -20,6 +20,7 @@ Issue:
-
lock_version
-
lock_version
-
milestone_id
-
milestone_id
-
weight
-
weight
-
time_estimate
Event
:
Event
:
-
id
-
id
-
target_type
-
target_type
...
@@ -150,6 +151,7 @@ MergeRequest:
...
@@ -150,6 +151,7 @@ MergeRequest:
-
milestone_id
-
milestone_id
-
approvals_before_merge
-
approvals_before_merge
-
rebase_commit_sha
-
rebase_commit_sha
-
time_estimate
MergeRequestDiff
:
MergeRequestDiff
:
-
id
-
id
-
state
-
state
...
@@ -344,3 +346,11 @@ LabelPriority:
...
@@ -344,3 +346,11 @@ LabelPriority:
-
priority
-
priority
-
created_at
-
created_at
-
updated_at
-
updated_at
Timelog
:
-
id
-
time_spent
-
trackable_id
-
trackable_type
-
user_id
-
created_at
-
updated_at
spec/models/concerns/issuable_spec.rb
View file @
17196a2f
...
@@ -408,4 +408,42 @@ describe Issue, "Issuable" do
...
@@ -408,4 +408,42 @@ describe Issue, "Issuable" do
expect
(
issue
.
assignee_or_author?
(
user
)).
to
eq
(
false
)
expect
(
issue
.
assignee_or_author?
(
user
)).
to
eq
(
false
)
end
end
end
end
describe
'#spend_time'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:issue
)
{
create
(
:issue
)
}
def
spend_time
(
seconds
)
issue
.
spend_time
(
seconds
,
user
)
issue
.
save!
end
context
'adding time'
do
it
'should update the total time spent'
do
spend_time
(
1800
)
expect
(
issue
.
total_time_spent
).
to
eq
(
1800
)
end
end
context
'substracting time'
do
before
do
spend_time
(
1800
)
end
it
'should update the total time spent'
do
spend_time
(
-
900
)
expect
(
issue
.
total_time_spent
).
to
eq
(
900
)
end
context
'when time to substract exceeds the total time spent'
do
it
'should not alter the total time spent'
do
spend_time
(
-
3600
)
expect
(
issue
.
total_time_spent
).
to
eq
(
1800
)
end
end
end
end
end
end
spec/models/timelog_spec.rb
0 → 100644
View file @
17196a2f
require
'rails_helper'
RSpec
.
describe
Timelog
,
type: :model
do
subject
{
build
(
:timelog
)
}
it
{
is_expected
.
to
be_valid
}
it
{
is_expected
.
to
validate_presence_of
(
:time_spent
)
}
it
{
is_expected
.
to
validate_presence_of
(
:user
)
}
end
spec/services/notes/slash_commands_service_spec.rb
View file @
17196a2f
...
@@ -86,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do
...
@@ -86,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do
expect
(
note
.
noteable
).
to
be_open
expect
(
note
.
noteable
).
to
be_open
end
end
end
end
describe
'/spend'
do
let
(
:note_text
)
{
'/spend 1h'
}
it
'updates the spent time on the noteable'
do
content
,
command_params
=
service
.
extract_commands
(
note
)
service
.
execute
(
command_params
,
note
)
expect
(
content
).
to
eq
''
expect
(
note
.
noteable
.
time_spent
).
to
eq
(
3600
)
end
end
end
end
describe
'note with command & text'
do
describe
'note with command & text'
do
...
...
spec/services/slash_commands/interpret_service_spec.rb
View file @
17196a2f
...
@@ -210,6 +210,46 @@ describe SlashCommands::InterpretService, services: true do
...
@@ -210,6 +210,46 @@ describe SlashCommands::InterpretService, services: true do
end
end
end
end
shared_examples
'estimate command'
do
it
'populates time_estimate: 3600 if content contains /estimate 1h'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
time_estimate:
3600
)
end
end
shared_examples
'spend command'
do
it
'populates spend_time: 3600 if content contains /spend 1h'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
3600
)
end
end
shared_examples
'spend command with negative time'
do
it
'populates spend_time: -1800 if content contains /spend -30m'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
-
1800
)
end
end
shared_examples
'remove_estimate command'
do
it
'populates time_estimate: 0 if content contains /remove_estimate'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
time_estimate:
0
)
end
end
shared_examples
'remove_time_spent command'
do
it
'populates spend_time: :reset if content contains /remove_time_spent'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time: :reset
)
end
end
shared_examples
'empty command'
do
shared_examples
'empty command'
do
it
'populates {} if content contains an unsupported command'
do
it
'populates {} if content contains an unsupported command'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
_
,
updates
=
service
.
execute
(
content
,
issuable
)
...
@@ -451,6 +491,51 @@ describe SlashCommands::InterpretService, services: true do
...
@@ -451,6 +491,51 @@ describe SlashCommands::InterpretService, services: true do
let
(
:issuable
)
{
merge_request
}
let
(
:issuable
)
{
merge_request
}
end
end
it_behaves_like
'estimate command'
do
let
(
:content
)
{
'/estimate 1h'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'empty command'
do
let
(
:content
)
{
'/estimate'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'empty command'
do
let
(
:content
)
{
'/estimate abc'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'spend command'
do
let
(
:content
)
{
'/spend 1h'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'spend command with negative time'
do
let
(
:content
)
{
'/spend -30m'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'empty command'
do
let
(
:content
)
{
'/spend'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'empty command'
do
let
(
:content
)
{
'/spend abc'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'remove_estimate command'
do
let
(
:content
)
{
'/remove_estimate'
}
let
(
:issuable
)
{
issue
}
end
it_behaves_like
'remove_time_spent command'
do
let
(
:content
)
{
'/remove_time_spent'
}
let
(
:issuable
)
{
issue
}
end
context
'when current_user cannot :admin_issue'
do
context
'when current_user cannot :admin_issue'
do
let
(
:visitor
)
{
create
(
:user
)
}
let
(
:visitor
)
{
create
(
:user
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
visitor
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
visitor
)
}
...
...
spec/services/system_note_service_spec.rb
View file @
17196a2f
...
@@ -740,4 +740,69 @@ describe SystemNoteService, services: true do
...
@@ -740,4 +740,69 @@ describe SystemNoteService, services: true do
expect
(
note
.
note
).
to
include
(
issue
.
to_reference
)
expect
(
note
.
note
).
to
include
(
issue
.
to_reference
)
end
end
end
end
describe
'.change_time_estimate'
do
subject
{
described_class
.
change_time_estimate
(
noteable
,
project
,
author
)
}
it_behaves_like
'a system note'
context
'with a time estimate'
do
it
'sets the note text'
do
noteable
.
update_attribute
(
:time_estimate
,
277200
)
expect
(
subject
.
note
).
to
eq
"Changed time estimate of this issue to 1w 4d 5h"
end
end
context
'without a time estimate'
do
it
'sets the note text'
do
expect
(
subject
.
note
).
to
eq
"Removed time estimate on this issue"
end
end
end
describe
'.change_time_spent'
do
# We need a custom noteable in order to the shared examples to be green.
let
(
:noteable
)
do
mr
=
create
(
:merge_request
,
source_project:
project
)
mr
.
spend_time
(
1
,
author
)
mr
.
save!
mr
end
subject
do
described_class
.
change_time_spent
(
noteable
,
project
,
author
)
end
it_behaves_like
'a system note'
context
'when time was added'
do
it
'sets the note text'
do
spend_time!
(
277200
)
expect
(
subject
.
note
).
to
eq
"Added 1w 4d 5h of time spent on this merge request"
end
end
context
'when time was subtracted'
do
it
'sets the note text'
do
spend_time!
(
-
277200
)
expect
(
subject
.
note
).
to
eq
"Subtracted 1w 4d 5h of time spent on this merge request"
end
end
context
'when time was removed'
do
it
'sets the note text'
do
spend_time!
(
:reset
)
expect
(
subject
.
note
).
to
eq
"Removed time spent on this merge request"
end
end
def
spend_time!
(
seconds
)
noteable
.
spend_time
(
seconds
,
author
)
noteable
.
save!
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment