diff --git a/conf/app.ini b/conf/app.ini
index 3333434230..676321d7d5 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -265,6 +265,12 @@ DEFAULT_KEEP_EMAIL_PRIVATE = false
 ; Default value for AllowCreateOrganization
 ; New user will have rights set to create organizations depending on this setting
 DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+; Default value for EnableTimetracking
+; Repositories will use timetracking by default depending on this setting
+DEFAULT_ENABLE_TIMETRACKING = true
+; Default value for AllowOnlyContributorsToTrackTime
+; Only users with write permissions could track time if this is true
+DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = true
 ; Default value for the domain part of the user's email address in the git log
 ; if he has set KeepEmailPrivate true. The user's email replaced with a
 ; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS.
diff --git a/integrations/html_helper.go b/integrations/html_helper.go
index 43e75db30d..c181a25b57 100644
--- a/integrations/html_helper.go
+++ b/integrations/html_helper.go
@@ -40,3 +40,13 @@ func (doc *HTMLDoc) GetInputValueByName(name string) string {
 func (doc *HTMLDoc) GetCSRF() string {
 	return doc.GetInputValueByName("_csrf")
 }
+
+// AssertElement check if element by selector exists or does not exist depending on checkExists
+func (doc *HTMLDoc) AssertElement(t testing.TB, selector string, checkExists bool) {
+	sel := doc.doc.Find(selector)
+	if checkExists {
+		assert.Equal(t, 1, sel.Length())
+	} else {
+		assert.Equal(t, 0, sel.Length())
+	}
+}
diff --git a/integrations/timetracking_test.go b/integrations/timetracking_test.go
new file mode 100644
index 0000000000..e92cb95a89
--- /dev/null
+++ b/integrations/timetracking_test.go
@@ -0,0 +1,74 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+	"net/http"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestViewTimetrackingControls(t *testing.T) {
+	prepareTestEnv(t)
+	session := loginUser(t, "user2")
+	testViewTimetrackingControls(t, session, "user2", "repo1", "1", true)
+	//user2/repo1
+}
+
+func TestNotViewTimetrackingControls(t *testing.T) {
+	prepareTestEnv(t)
+	session := loginUser(t, "user5")
+	testViewTimetrackingControls(t, session, "user2", "repo1", "1", false)
+	//user2/repo1
+}
+func TestViewTimetrackingControlsDisabled(t *testing.T) {
+	prepareTestEnv(t)
+	session := loginUser(t, "user2")
+	testViewTimetrackingControls(t, session, "user3", "repo3", "1", false)
+}
+
+func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) {
+	req := NewRequest(t, "GET", path.Join(user, repo, "issues", issue))
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	htmlDoc.AssertElement(t, ".timetrack .start-add .start", canTrackTime)
+	htmlDoc.AssertElement(t, ".timetrack .start-add .add-time", canTrackTime)
+
+	req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+	})
+	if canTrackTime {
+		resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+		req = NewRequest(t, "GET", RedirectURL(t, resp))
+		resp = session.MakeRequest(t, req, http.StatusOK)
+		htmlDoc = NewHTMLParser(t, resp.Body)
+
+		events := htmlDoc.doc.Find(".event > span.text")
+		assert.Contains(t, events.Last().Text(), "started working")
+
+		htmlDoc.AssertElement(t, ".timetrack .stop-cancel .stop", true)
+		htmlDoc.AssertElement(t, ".timetrack .stop-cancel .cancel", true)
+
+		req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{
+			"_csrf": htmlDoc.GetCSRF(),
+		})
+		resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+		req = NewRequest(t, "GET", RedirectURL(t, resp))
+		resp = session.MakeRequest(t, req, http.StatusOK)
+		htmlDoc = NewHTMLParser(t, resp.Body)
+
+		events = htmlDoc.doc.Find(".event > span.text")
+		assert.Contains(t, events.Last().Text(), "stopped working")
+		htmlDoc.AssertElement(t, ".event .detail .octicon-clock", true)
+	} else {
+		session.MakeRequest(t, req, http.StatusNotFound)
+	}
+}
diff --git a/models/error.go b/models/error.go
index 2adb4b45e1..37505e8aa5 100644
--- a/models/error.go
+++ b/models/error.go
@@ -768,6 +768,50 @@ func (err ErrCommentNotExist) Error() string {
 	return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
 }
 
+//  _________ __                                __         .__
+//  /   _____//  |_  ____ ________  _  _______ _/  |_  ____ |  |__
+//  \_____  \\   __\/  _ \\____ \ \/ \/ /\__  \\   __\/ ___\|  |  \
+//  /        \|  | (  <_> )  |_> >     /  / __ \|  | \  \___|   Y  \
+//  /_______  /|__|  \____/|   __/ \/\_/  (____  /__|  \___  >___|  /
+// \/             |__|                \/          \/     \/
+
+// ErrStopwatchNotExist represents a "Stopwatch Not Exist" kind of error.
+type ErrStopwatchNotExist struct {
+	ID int64
+}
+
+// IsErrStopwatchNotExist checks if an error is a ErrStopwatchNotExist.
+func IsErrStopwatchNotExist(err error) bool {
+	_, ok := err.(ErrStopwatchNotExist)
+	return ok
+}
+
+func (err ErrStopwatchNotExist) Error() string {
+	return fmt.Sprintf("stopwatch does not exist [id: %d]", err.ID)
+}
+
+// ___________                     __              .______________.__
+// \__    ___/___________    ____ |  | __ ____   __| _/\__    ___/|__| _____   ____
+// |    |  \_  __ \__  \ _/ ___\|  |/ // __ \ / __ |   |    |   |  |/     \_/ __ \
+// |    |   |  | \// __ \\  \___|    <\  ___// /_/ |   |    |   |  |  Y Y  \  ___/
+// |____|   |__|  (____  /\___  >__|_ \\___  >____ |   |____|   |__|__|_|  /\___  >
+// \/     \/     \/    \/     \/                     \/     \/
+
+// ErrTrackedTimeNotExist represents a "TrackedTime Not Exist" kind of error.
+type ErrTrackedTimeNotExist struct {
+	ID int64
+}
+
+// IsErrTrackedTimeNotExist checks if an error is a ErrTrackedTimeNotExist.
+func IsErrTrackedTimeNotExist(err error) bool {
+	_, ok := err.(ErrTrackedTimeNotExist)
+	return ok
+}
+
+func (err ErrTrackedTimeNotExist) Error() string {
+	return fmt.Sprintf("tracked time does not exist [id: %d]", err.ID)
+}
+
 // .____          ___.          .__
 // |    |   _____ \_ |__   ____ |  |
 // |    |   \__  \ | __ \_/ __ \|  |
diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml
index 3292bb4848..34df02d28c 100644
--- a/models/fixtures/comment.yml
+++ b/models/fixtures/comment.yml
@@ -5,15 +5,18 @@
   issue_id: 1 # in repo_id 1
   label_id: 1
   content: "1"
+  created_unix: 946684810
 -
   id: 2
   type: 0 # comment
   poster_id: 3 # user not watching (see watch.yml)
   issue_id: 1 # in repo_id 1
   content: "good work!"
+  created_unix: 946684811
 -
   id: 3
   type: 0 # comment
   poster_id: 5 # user not watching (see watch.yml)
   issue_id: 1 # in repo_id 1
   content: "meh..."
+  created_unix: 946684812
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 7bbbab26fe..b80ada1ba4 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -57,3 +57,16 @@
   content: content5
   is_closed: true
   is_pull: false
+-
+  id: 6
+  repo_id: 3
+  index: 1
+  poster_id: 1
+  assignee_id: 1
+  name: issue6
+  content: content6
+  is_closed: false
+  is_pull: false
+  num_comments: 0
+  created_unix: 946684800
+  updated_unix: 978307200
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 02daa48277..57cf35e198 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -11,7 +11,7 @@
   repo_id: 1
   type: 2
   index: 1
-  config: "{}"
+  config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
   created_unix: 946684810
 
 -
@@ -51,7 +51,7 @@
   repo_id: 3
   type: 2
   index: 1
-  config: "{}"
+  config: "{\"EnableTimetracker\":false,\"AllowOnlyContributorsToTrackTime\":false}"
   created_unix: 946684810
 
 -
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index b8f607b2a8..3409ba8113 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -29,7 +29,7 @@
   lower_name: repo3
   name: repo3
   is_private: true
-  num_issues: 0
+  num_issues: 1
   num_closed_issues: 0
   num_pulls: 0
   num_closed_pulls: 0
diff --git a/models/fixtures/stopwatch.yml b/models/fixtures/stopwatch.yml
new file mode 100644
index 0000000000..397a8214d4
--- /dev/null
+++ b/models/fixtures/stopwatch.yml
@@ -0,0 +1,11 @@
+-
+  id: 1
+  user_id: 1
+  issue_id: 1
+  created_unix: 1500988502
+
+-
+  id: 2
+  user_id: 2
+  issue_id: 2
+  created_unix: 1500988502
diff --git a/models/fixtures/tracked_time.yml b/models/fixtures/tracked_time.yml
new file mode 100644
index 0000000000..06a71c5ad9
--- /dev/null
+++ b/models/fixtures/tracked_time.yml
@@ -0,0 +1,34 @@
+-
+  id: 1
+  user_id: 1
+  issue_id: 1
+  time: 400
+  created_unix: 946684800
+
+-
+  id: 2
+  user_id: 2
+  issue_id: 2
+  time: 3661
+  created_unix: 946684801
+
+-
+  id: 3
+  user_id: 2
+  issue_id: 2
+  time: 1
+  created_unix: 946684802
+
+-
+  id: 4
+  user_id: -1
+  issue_id: 4
+  time: 1
+  created_unix: 946684802
+
+-
+  id: 5
+  user_id: 2
+  issue_id: 5
+  time: 1
+  created_unix: 946684802
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 79fa23960d..fe2223e9d2 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -52,6 +52,14 @@ const (
 	CommentTypeChangeTitle
 	// Delete Branch
 	CommentTypeDeleteBranch
+	// Start a stopwatch for time tracking
+	CommentTypeStartTracking
+	// Stop a stopwatch for time tracking
+	CommentTypeStopTracking
+	// Add time manual for time tracking
+	CommentTypeAddTimeManual
+	// Cancel a stopwatch for time tracking
+	CommentTypeCancelTracking
 )
 
 // CommentTag defines comment tag type
@@ -672,7 +680,6 @@ func DeleteComment(comment *Comment) error {
 			return err
 		}
 	}
-
 	if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
 		return err
 	}
diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go
new file mode 100644
index 0000000000..3b5b4d57f3
--- /dev/null
+++ b/models/issue_stopwatch.go
@@ -0,0 +1,170 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/go-xorm/xorm"
+)
+
+// Stopwatch represents a stopwatch for time tracking.
+type Stopwatch struct {
+	ID          int64     `xorm:"pk autoincr"`
+	IssueID     int64     `xorm:"INDEX"`
+	UserID      int64     `xorm:"INDEX"`
+	Created     time.Time `xorm:"-"`
+	CreatedUnix int64
+}
+
+// BeforeInsert will be invoked by XORM before inserting a record
+// representing this object.
+func (s *Stopwatch) BeforeInsert() {
+	s.CreatedUnix = time.Now().Unix()
+}
+
+// AfterSet is invoked from XORM after setting the value of a field of this object.
+func (s *Stopwatch) AfterSet(colName string, _ xorm.Cell) {
+	switch colName {
+
+	case "created_unix":
+		s.Created = time.Unix(s.CreatedUnix, 0).Local()
+	}
+}
+
+func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
+	sw = new(Stopwatch)
+	exists, err = e.
+		Where("user_id = ?", userID).
+		And("issue_id = ?", issueID).
+		Get(sw)
+	return
+}
+
+// StopwatchExists returns true if the stopwatch exists
+func StopwatchExists(userID int64, issueID int64) bool {
+	_, exists, _ := getStopwatch(x, userID, issueID)
+	return exists
+}
+
+// HasUserStopwatch returns true if the user has a stopwatch
+func HasUserStopwatch(userID int64) (exists bool, sw *Stopwatch, err error) {
+	sw = new(Stopwatch)
+	exists, err = x.
+		Where("user_id = ?", userID).
+		Get(sw)
+	return
+}
+
+// CreateOrStopIssueStopwatch will create or remove a stopwatch and will log it into issue's timeline.
+func CreateOrStopIssueStopwatch(user *User, issue *Issue) error {
+	sw, exists, err := getStopwatch(x, user.ID, issue.ID)
+	if err != nil {
+		return err
+	}
+	if exists {
+		// Create tracked time out of the time difference between start date and actual date
+		timediff := time.Now().Unix() - sw.CreatedUnix
+
+		// Create TrackedTime
+		tt := &TrackedTime{
+			Created: time.Now(),
+			IssueID: issue.ID,
+			UserID:  user.ID,
+			Time:    timediff,
+		}
+
+		if _, err := x.Insert(tt); err != nil {
+			return err
+		}
+
+		if _, err := CreateComment(&CreateCommentOptions{
+			Doer:    user,
+			Issue:   issue,
+			Repo:    issue.Repo,
+			Content: secToTime(timediff),
+			Type:    CommentTypeStopTracking,
+		}); err != nil {
+			return err
+		}
+		if _, err := x.Delete(sw); err != nil {
+			return err
+		}
+	} else {
+		// Create stopwatch
+		sw = &Stopwatch{
+			UserID:  user.ID,
+			IssueID: issue.ID,
+			Created: time.Now(),
+		}
+
+		if _, err := x.Insert(sw); err != nil {
+			return err
+		}
+
+		if _, err := CreateComment(&CreateCommentOptions{
+			Doer:  user,
+			Issue: issue,
+			Repo:  issue.Repo,
+			Type:  CommentTypeStartTracking,
+		}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
+func CancelStopwatch(user *User, issue *Issue) error {
+	sw, exists, err := getStopwatch(x, user.ID, issue.ID)
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		if _, err := x.Delete(sw); err != nil {
+			return err
+		}
+
+		if _, err := CreateComment(&CreateCommentOptions{
+			Doer:  user,
+			Issue: issue,
+			Repo:  issue.Repo,
+			Type:  CommentTypeCancelTracking,
+		}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func secToTime(duration int64) string {
+	seconds := duration % 60
+	minutes := (duration / (60)) % 60
+	hours := duration / (60 * 60)
+
+	var hrs string
+
+	if hours > 0 {
+		hrs = fmt.Sprintf("%dh", hours)
+	}
+	if minutes > 0 {
+		if hours == 0 {
+			hrs = fmt.Sprintf("%dmin", minutes)
+		} else {
+			hrs = fmt.Sprintf("%s %dmin", hrs, minutes)
+		}
+	}
+	if seconds > 0 {
+		if hours == 0 && minutes == 0 {
+			hrs = fmt.Sprintf("%ds", seconds)
+		} else {
+			hrs = fmt.Sprintf("%s %ds", hrs, seconds)
+		}
+	}
+
+	return hrs
+}
diff --git a/models/issue_stopwatch_test.go b/models/issue_stopwatch_test.go
new file mode 100644
index 0000000000..9875066e53
--- /dev/null
+++ b/models/issue_stopwatch_test.go
@@ -0,0 +1,70 @@
+package models
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCancelStopwatch(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	user1, err := GetUserByID(1)
+	assert.NoError(t, err)
+
+	issue1, err := GetIssueByID(1)
+	assert.NoError(t, err)
+	issue2, err := GetIssueByID(2)
+	assert.NoError(t, err)
+
+	err = CancelStopwatch(user1, issue1)
+	assert.NoError(t, err)
+	AssertNotExistsBean(t, &Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
+
+	_ = AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
+
+	assert.Nil(t, CancelStopwatch(user1, issue2))
+}
+
+func TestStopwatchExists(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	assert.True(t, StopwatchExists(1, 1))
+	assert.False(t, StopwatchExists(1, 2))
+}
+
+func TestHasUserStopwatch(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	exists, sw, err := HasUserStopwatch(1)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+	assert.Equal(t, int64(1), sw.ID)
+
+	exists, _, err = HasUserStopwatch(3)
+	assert.NoError(t, err)
+	assert.False(t, exists)
+}
+
+func TestCreateOrStopIssueStopwatch(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	user2, err := GetUserByID(2)
+	assert.NoError(t, err)
+	user3, err := GetUserByID(3)
+	assert.NoError(t, err)
+
+	issue1, err := GetIssueByID(1)
+	assert.NoError(t, err)
+	issue2, err := GetIssueByID(2)
+	assert.NoError(t, err)
+
+	assert.NoError(t, CreateOrStopIssueStopwatch(user3, issue1))
+	sw := AssertExistsAndLoadBean(t, &Stopwatch{UserID: 3, IssueID: 1}).(*Stopwatch)
+	assert.Equal(t, true, sw.Created.Before(time.Now()))
+
+	assert.NoError(t, CreateOrStopIssueStopwatch(user2, issue2))
+	AssertNotExistsBean(t, &Stopwatch{UserID: 2, IssueID: 2})
+	AssertExistsAndLoadBean(t, &TrackedTime{UserID: 2, IssueID: 2})
+}
diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go
new file mode 100644
index 0000000000..33914dbb1f
--- /dev/null
+++ b/models/issue_tracked_time.go
@@ -0,0 +1,117 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"time"
+
+	"github.com/go-xorm/builder"
+	"github.com/go-xorm/xorm"
+)
+
+// TrackedTime represents a time that was spent for a specific issue.
+type TrackedTime struct {
+	ID          int64     `xorm:"pk autoincr" json:"id"`
+	IssueID     int64     `xorm:"INDEX" json:"issue_id"`
+	UserID      int64     `xorm:"INDEX" json:"user_id"`
+	Created     time.Time `xorm:"-" json:"created"`
+	CreatedUnix int64     `json:"-"`
+	Time        int64     `json:"time"`
+}
+
+// BeforeInsert will be invoked by XORM before inserting a record
+// representing this object.
+func (t *TrackedTime) BeforeInsert() {
+	t.CreatedUnix = time.Now().Unix()
+}
+
+// AfterSet is invoked from XORM after setting the value of a field of this object.
+func (t *TrackedTime) AfterSet(colName string, _ xorm.Cell) {
+	switch colName {
+	case "created_unix":
+		t.Created = time.Unix(t.CreatedUnix, 0).Local()
+	}
+}
+
+// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
+type FindTrackedTimesOptions struct {
+	IssueID      int64
+	UserID       int64
+	RepositoryID int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
+	cond := builder.NewCond()
+	if opts.IssueID != 0 {
+		cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
+	}
+	if opts.UserID != 0 {
+		cond = cond.And(builder.Eq{"user_id": opts.UserID})
+	}
+	if opts.RepositoryID != 0 {
+		cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
+	}
+	return cond
+}
+
+// GetTrackedTimes returns all tracked times that fit to the given options.
+func GetTrackedTimes(options FindTrackedTimesOptions) (trackedTimes []*TrackedTime, err error) {
+	if options.RepositoryID > 0 {
+		err = x.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(options.ToCond()).Find(&trackedTimes)
+		return
+	}
+	err = x.Where(options.ToCond()).Find(&trackedTimes)
+	return
+}
+
+// AddTime will add the given time (in seconds) to the issue
+func AddTime(user *User, issue *Issue, time int64) (*TrackedTime, error) {
+	tt := &TrackedTime{
+		IssueID: issue.ID,
+		UserID:  user.ID,
+		Time:    time,
+	}
+	if _, err := x.Insert(tt); err != nil {
+		return nil, err
+	}
+	if _, err := CreateComment(&CreateCommentOptions{
+		Issue:   issue,
+		Repo:    issue.Repo,
+		Doer:    user,
+		Content: secToTime(time),
+		Type:    CommentTypeAddTimeManual,
+	}); err != nil {
+		return nil, err
+	}
+	return tt, nil
+}
+
+// TotalTimes returns the spent time for each user by an issue
+func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) {
+	trackedTimes, err := GetTrackedTimes(options)
+	if err != nil {
+		return nil, err
+	}
+	//Adding total time per user ID
+	totalTimesByUser := make(map[int64]int64)
+	for _, t := range trackedTimes {
+		totalTimesByUser[t.UserID] += t.Time
+	}
+
+	totalTimes := make(map[*User]string)
+	//Fetching User and making time human readable
+	for userID, total := range totalTimesByUser {
+		user, err := GetUserByID(userID)
+		if err != nil {
+			if IsErrUserNotExist(err) {
+				continue
+			}
+			return nil, err
+		}
+		totalTimes[user] = secToTime(total)
+	}
+	return totalTimes, nil
+}
diff --git a/models/issue_tracked_time_test.go b/models/issue_tracked_time_test.go
new file mode 100644
index 0000000000..130e8f33e2
--- /dev/null
+++ b/models/issue_tracked_time_test.go
@@ -0,0 +1,103 @@
+package models
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAddTime(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	user3, err := GetUserByID(3)
+	assert.NoError(t, err)
+
+	issue1, err := GetIssueByID(1)
+	assert.NoError(t, err)
+
+	//3661 = 1h 1min 1s
+	trackedTime, err := AddTime(user3, issue1, 3661)
+	assert.NoError(t, err)
+	assert.Equal(t, int64(3), trackedTime.UserID)
+	assert.Equal(t, int64(1), trackedTime.IssueID)
+	assert.Equal(t, int64(3661), trackedTime.Time)
+
+	tt := AssertExistsAndLoadBean(t, &TrackedTime{UserID: 3, IssueID: 1}).(*TrackedTime)
+	assert.Equal(t, tt.Time, int64(3661))
+
+	comment := AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
+	assert.Equal(t, comment.Content, "1h 1min 1s")
+}
+
+func TestGetTrackedTimes(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	// by Issue
+	times, err := GetTrackedTimes(FindTrackedTimesOptions{IssueID: 1})
+	assert.NoError(t, err)
+	assert.Len(t, times, 1)
+	assert.Equal(t, times[0].Time, int64(400))
+
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{IssueID: -1})
+	assert.NoError(t, err)
+	assert.Len(t, times, 0)
+
+	// by User
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 1})
+	assert.NoError(t, err)
+	assert.Len(t, times, 1)
+	assert.Equal(t, times[0].Time, int64(400))
+
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 3})
+	assert.NoError(t, err)
+	assert.Len(t, times, 0)
+
+	// by Repo
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 2})
+	assert.NoError(t, err)
+	assert.Len(t, times, 1)
+	assert.Equal(t, times[0].Time, int64(1))
+	issue, err := GetIssueByID(times[0].IssueID)
+	assert.NoError(t, err)
+	assert.Equal(t, issue.RepoID, int64(2))
+
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 1})
+	assert.NoError(t, err)
+	assert.Len(t, times, 4)
+
+	times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 10})
+	assert.NoError(t, err)
+	assert.Len(t, times, 0)
+}
+
+func TestTotalTimes(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	total, err := TotalTimes(FindTrackedTimesOptions{IssueID: 1})
+	assert.NoError(t, err)
+	assert.Len(t, total, 1)
+	for user, time := range total {
+		assert.Equal(t, int64(1), user.ID)
+		assert.Equal(t, "6min 40s", time)
+	}
+
+	total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 2})
+	assert.NoError(t, err)
+	assert.Len(t, total, 1)
+	for user, time := range total {
+		assert.Equal(t, int64(2), user.ID)
+		assert.Equal(t, "1h 1min 2s", time)
+	}
+
+	total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 5})
+	assert.NoError(t, err)
+	assert.Len(t, total, 1)
+	for user, time := range total {
+		assert.Equal(t, int64(2), user.ID)
+		assert.Equal(t, "1s", time)
+	}
+
+	total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 4})
+	assert.NoError(t, err)
+	assert.Len(t, total, 0)
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 75ef8f6c58..a796c6d6af 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -126,6 +126,8 @@ var migrations = []Migration{
 	NewMigration("unescape user full names", unescapeUserFullNames),
 	// v38 -> v39
 	NewMigration("remove commits and settings unit types", removeCommitsUnitType),
+	// v39 -> v40
+	NewMigration("adds time tracking and stopwatches", addTimetracking),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v16.go b/models/migrations/v16.go
index 9cd4ef4da2..2a6d71de41 100644
--- a/models/migrations/v16.go
+++ b/models/migrations/v16.go
@@ -19,9 +19,9 @@ type RepoUnit struct {
 	RepoID      int64 `xorm:"INDEX(s)"`
 	Type        int   `xorm:"INDEX(s)"`
 	Index       int
-	Config      map[string]string `xorm:"JSON"`
-	CreatedUnix int64             `xorm:"INDEX CREATED"`
-	Created     time.Time         `xorm:"-"`
+	Config      map[string]interface{} `xorm:"JSON"`
+	CreatedUnix int64                  `xorm:"INDEX CREATED"`
+	Created     time.Time              `xorm:"-"`
 }
 
 // Enumerate all the unit types
@@ -95,7 +95,7 @@ func addUnitsToTables(x *xorm.Engine) error {
 				continue
 			}
 
-			var config = make(map[string]string)
+			var config = make(map[string]interface{})
 			switch i {
 			case V16UnitTypeExternalTracker:
 				config["ExternalTrackerURL"] = repo.ExternalTrackerURL
diff --git a/models/migrations/v39.go b/models/migrations/v39.go
new file mode 100644
index 0000000000..a536cd9d62
--- /dev/null
+++ b/models/migrations/v39.go
@@ -0,0 +1,65 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/go-xorm/xorm"
+)
+
+// Stopwatch see models/issue_stopwatch.go
+type Stopwatch struct {
+	ID          int64     `xorm:"pk autoincr"`
+	IssueID     int64     `xorm:"INDEX"`
+	UserID      int64     `xorm:"INDEX"`
+	Created     time.Time `xorm:"-"`
+	CreatedUnix int64
+}
+
+// TrackedTime see models/issue_tracked_time.go
+type TrackedTime struct {
+	ID          int64     `xorm:"pk autoincr" json:"id"`
+	IssueID     int64     `xorm:"INDEX" json:"issue_id"`
+	UserID      int64     `xorm:"INDEX" json:"user_id"`
+	Created     time.Time `xorm:"-" json:"created"`
+	CreatedUnix int64     `json:"-"`
+	Time        int64     `json:"time"`
+}
+
+func addTimetracking(x *xorm.Engine) error {
+	if err := x.Sync2(new(Stopwatch)); err != nil {
+		return fmt.Errorf("Sync2: %v", err)
+	}
+	if err := x.Sync2(new(TrackedTime)); err != nil {
+		return fmt.Errorf("Sync2: %v", err)
+	}
+	//Updating existing issue units
+	var units []*RepoUnit
+	x.Where("type = ?", V16UnitTypeIssues).Find(&units)
+	for _, unit := range units {
+		if unit.Config == nil {
+			unit.Config = make(map[string]interface{})
+		}
+		changes := false
+		if _, ok := unit.Config["EnableTimetracker"]; !ok {
+			unit.Config["EnableTimetracker"] = setting.Service.DefaultEnableTimetracking
+			changes = true
+		}
+		if _, ok := unit.Config["AllowOnlyContributorsToTrackTime"]; !ok {
+			unit.Config["AllowOnlyContributorsToTrackTime"] = setting.Service.DefaultAllowOnlyContributorsToTrackTime
+			changes = true
+		}
+		if changes {
+			if _, err := x.Id(unit.ID).Cols("config").Update(unit); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/models/models.go b/models/models.go
index e5d3597ce7..0bcb3674da 100644
--- a/models/models.go
+++ b/models/models.go
@@ -112,6 +112,8 @@ func init() {
 		new(UserOpenID),
 		new(IssueWatch),
 		new(CommitStatus),
+		new(Stopwatch),
+		new(TrackedTime),
 	)
 
 	gonicNames := []string{"SSL", "UID"}
diff --git a/models/repo.go b/models/repo.go
index a629b7311f..1cce3854ee 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -32,8 +32,8 @@ import (
 	"github.com/Unknwon/cae/zip"
 	"github.com/Unknwon/com"
 	"github.com/go-xorm/xorm"
-	version "github.com/mcuadros/go-version"
-	ini "gopkg.in/ini.v1"
+	"github.com/mcuadros/go-version"
+	"gopkg.in/ini.v1"
 )
 
 const (
@@ -1224,11 +1224,21 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
 	// insert units for repo
 	var units = make([]RepoUnit, 0, len(defaultRepoUnits))
 	for i, tp := range defaultRepoUnits {
-		units = append(units, RepoUnit{
-			RepoID: repo.ID,
-			Type:   tp,
-			Index:  i,
-		})
+		if tp == UnitTypeIssues {
+			units = append(units, RepoUnit{
+				RepoID: repo.ID,
+				Type:   tp,
+				Index:  i,
+				Config: &IssuesConfig{EnableTimetracker: setting.Service.DefaultEnableTimetracking, AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime},
+			})
+		} else {
+			units = append(units, RepoUnit{
+				RepoID: repo.ID,
+				Type:   tp,
+				Index:  i,
+			})
+		}
+
 	}
 
 	if _, err = e.Insert(&units); err != nil {
diff --git a/models/repo_issue.go b/models/repo_issue.go
new file mode 100644
index 0000000000..10356d2c98
--- /dev/null
+++ b/models/repo_issue.go
@@ -0,0 +1,34 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import "code.gitea.io/gitea/modules/setting"
+
+// ___________.__             ___________                     __
+// \__    ___/|__| _____   ___\__    ___/___________    ____ |  | __ ___________
+// |    |   |  |/     \_/ __ \|    |  \_  __ \__  \ _/ ___\|  |/ // __ \_  __ \
+// |    |   |  |  Y Y  \  ___/|    |   |  | \// __ \\  \___|    <\  ___/|  | \/
+// |____|   |__|__|_|  /\___  >____|   |__|  (____  /\___  >__|_ \\___  >__|
+// \/     \/                    \/     \/     \/    \/
+
+// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
+func (repo *Repository) IsTimetrackerEnabled() bool {
+	var u *RepoUnit
+	var err error
+	if u, err = repo.GetUnit(UnitTypeIssues); err != nil {
+		return setting.Service.DefaultEnableTimetracking
+	}
+	return u.IssuesConfig().EnableTimetracker
+}
+
+// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
+func (repo *Repository) AllowOnlyContributorsToTrackTime() bool {
+	var u *RepoUnit
+	var err error
+	if u, err = repo.GetUnit(UnitTypeIssues); err != nil {
+		return setting.Service.DefaultAllowOnlyContributorsToTrackTime
+	}
+	return u.IssuesConfig().AllowOnlyContributorsToTrackTime
+}
diff --git a/models/repo_unit.go b/models/repo_unit.go
index 2256ff7ef6..1553fa771d 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -70,18 +70,36 @@ func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
 	return json.Marshal(cfg)
 }
 
+// IssuesConfig describes issues config
+type IssuesConfig struct {
+	EnableTimetracker                bool
+	AllowOnlyContributorsToTrackTime bool
+}
+
+// FromDB fills up a IssuesConfig from serialized format.
+func (cfg *IssuesConfig) FromDB(bs []byte) error {
+	return json.Unmarshal(bs, &cfg)
+}
+
+// ToDB exports a IssuesConfig to a serialized format.
+func (cfg *IssuesConfig) ToDB() ([]byte, error) {
+	return json.Marshal(cfg)
+}
+
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
 	switch colName {
 	case "type":
 		switch UnitType(Cell2Int64(val)) {
-		case UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, UnitTypeReleases,
+		case UnitTypeCode, UnitTypePullRequests, UnitTypeReleases,
 			UnitTypeWiki:
 			r.Config = new(UnitConfig)
 		case UnitTypeExternalWiki:
 			r.Config = new(ExternalWikiConfig)
 		case UnitTypeExternalTracker:
 			r.Config = new(ExternalTrackerConfig)
+		case UnitTypeIssues:
+			r.Config = new(IssuesConfig)
 		default:
 			panic("unrecognized repo unit type: " + com.ToStr(*val))
 		}
@@ -106,11 +124,6 @@ func (r *RepoUnit) CodeConfig() *UnitConfig {
 	return r.Config.(*UnitConfig)
 }
 
-// IssuesConfig returns config for UnitTypeIssues
-func (r *RepoUnit) IssuesConfig() *UnitConfig {
-	return r.Config.(*UnitConfig)
-}
-
 // PullRequestsConfig returns config for UnitTypePullRequests
 func (r *RepoUnit) PullRequestsConfig() *UnitConfig {
 	return r.Config.(*UnitConfig)
@@ -126,6 +139,11 @@ func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
 	return r.Config.(*ExternalWikiConfig)
 }
 
+// IssuesConfig returns config for UnitTypeIssues
+func (r *RepoUnit) IssuesConfig() *IssuesConfig {
+	return r.Config.(*IssuesConfig)
+}
+
 // ExternalTrackerConfig returns config for UnitTypeExternalTracker
 func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
 	return r.Config.(*ExternalTrackerConfig)
diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go
index 2071cccb41..4e9d2bff61 100644
--- a/modules/auth/repo_form.go
+++ b/modules/auth/repo_form.go
@@ -12,7 +12,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"github.com/Unknwon/com"
 	"github.com/go-macaron/binding"
-	macaron "gopkg.in/macaron.v1"
+	"gopkg.in/macaron.v1"
 )
 
 // _______________________________________    _________.______________________ _______________.___.
@@ -95,15 +95,17 @@ type RepoSettingForm struct {
 	EnablePrune   bool
 
 	// Advanced settings
-	EnableWiki            bool
-	EnableExternalWiki    bool
-	ExternalWikiURL       string
-	EnableIssues          bool
-	EnableExternalTracker bool
-	ExternalTrackerURL    string
-	TrackerURLFormat      string
-	TrackerIssueStyle     string
-	EnablePulls           bool
+	EnableWiki                       bool
+	EnableExternalWiki               bool
+	ExternalWikiURL                  string
+	EnableIssues                     bool
+	EnableExternalTracker            bool
+	ExternalTrackerURL               string
+	TrackerURLFormat                 string
+	TrackerIssueStyle                string
+	EnablePulls                      bool
+	EnableTimetracker                bool
+	AllowOnlyContributorsToTrackTime bool
 }
 
 // Validate validates the fields
@@ -423,3 +425,21 @@ type DeleteRepoFileForm struct {
 func (f *DeleteRepoFileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
+
+// ___________.__                 ___________                     __
+// \__    ___/|__| _____   ____   \__    ___/___________    ____ |  | __ ___________
+// |    |   |  |/     \_/ __ \    |    |  \_  __ \__  \ _/ ___\|  |/ // __ \_  __ \
+// |    |   |  |  Y Y  \  ___/    |    |   |  | \// __ \\  \___|    <\  ___/|  | \/
+// |____|   |__|__|_|  /\___  >   |____|   |__|  (____  /\___  >__|_ \\___  >__|
+// \/     \/                        \/     \/     \/    \/
+
+// AddTimeManuallyForm form that adds spent time manually.
+type AddTimeManuallyForm struct {
+	Hours   int `binding:"Range(0,1000)"`
+	Minutes int `binding:"Range(0,1000)"`
+}
+
+// Validate validates the fields
+func (f *AddTimeManuallyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 3193717150..ab0bb1e7dd 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -48,6 +48,7 @@ type InstallForm struct {
 	RequireSignInView              bool
 	DefaultKeepEmailPrivate        bool
 	DefaultAllowCreateOrganization bool
+	DefaultEnableTimetracking      bool
 	NoReplyAddress                 string
 
 	AdminName          string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"`
diff --git a/modules/context/repo.go b/modules/context/repo.go
index b16d188428..e335eafde3 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -1,4 +1,5 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
@@ -14,8 +15,8 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/setting"
 	"github.com/Unknwon/com"
-	editorconfig "gopkg.in/editorconfig/editorconfig-core-go.v1"
-	macaron "gopkg.in/macaron.v1"
+	"gopkg.in/editorconfig/editorconfig-core-go.v1"
+	"gopkg.in/macaron.v1"
 )
 
 // PullRequest contains informations to make a pull request
@@ -85,6 +86,15 @@ func (r *Repository) CanCommitToBranch() (bool, error) {
 	return r.CanEnableEditor() && !protectedBranch, nil
 }
 
+// CanUseTimetracker returns whether or not a user can use the timetracker.
+func (r *Repository) CanUseTimetracker(issue *models.Issue, user *models.User) bool {
+	// Checking for following:
+	// 1. Is timetracker enabled
+	// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
+	return r.Repository.IsTimetrackerEnabled() && (!r.Repository.AllowOnlyContributorsToTrackTime() ||
+		r.IsWriter() || issue.IsPoster(user.ID) || issue.AssigneeID == user.ID)
+}
+
 // GetEditorconfig returns the .editorconfig definition if found in the
 // HEAD of the default repo branch.
 func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index a5908bacaa..00aaad6913 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -34,7 +34,7 @@ import (
 	"github.com/go-macaron/session"
 	_ "github.com/go-macaron/session/redis" // redis plugin for store session
 	"github.com/go-xorm/core"
-	ini "gopkg.in/ini.v1"
+	"gopkg.in/ini.v1"
 	"strk.kbt.io/projects/go/libravatar"
 )
 
@@ -1016,19 +1016,21 @@ func NewContext() {
 
 // Service settings
 var Service struct {
-	ActiveCodeLives                int
-	ResetPwdCodeLives              int
-	RegisterEmailConfirm           bool
-	DisableRegistration            bool
-	ShowRegistrationButton         bool
-	RequireSignInView              bool
-	EnableNotifyMail               bool
-	EnableReverseProxyAuth         bool
-	EnableReverseProxyAutoRegister bool
-	EnableCaptcha                  bool
-	DefaultKeepEmailPrivate        bool
-	DefaultAllowCreateOrganization bool
-	NoReplyAddress                 string
+	ActiveCodeLives                         int
+	ResetPwdCodeLives                       int
+	RegisterEmailConfirm                    bool
+	DisableRegistration                     bool
+	ShowRegistrationButton                  bool
+	RequireSignInView                       bool
+	EnableNotifyMail                        bool
+	EnableReverseProxyAuth                  bool
+	EnableReverseProxyAutoRegister          bool
+	EnableCaptcha                           bool
+	DefaultKeepEmailPrivate                 bool
+	DefaultAllowCreateOrganization          bool
+	DefaultEnableTimetracking               bool
+	DefaultAllowOnlyContributorsToTrackTime bool
+	NoReplyAddress                          string
 
 	// OpenID settings
 	EnableOpenIDSignIn bool
@@ -1049,6 +1051,8 @@ func newService() {
 	Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool()
 	Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
 	Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
+	Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
+	Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
 	Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
 
 	sec = Cfg.Section("openid")
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6cf41d8b90..f43badaab2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -144,6 +144,8 @@ default_keep_email_private = Default Value for Keep Email Private
 default_keep_email_private_popup = This is the default value for the visibility of the user's email address. If set to true the email address of all new users will be hidden until the user changes his setting.
 default_allow_create_organization = Default permission value for new users to create organizations
 default_allow_create_organization_popup = This is default permission value that will be assigned for new users. If set to true new users will be allowed to create Organizations.
+default_enable_timetracking = Enable time tracking by default
+default_enable_timetracking_popup = Repositories will have time tracking enabled by default depending on this setting
 no_reply_address = No-reply Address
 no_reply_address_helper = Domain for the user's email address in git logs if he keeps his email address private. E.g. user 'joe' and 'noreply.example.org' will be 'joe@noreply.example.org'
 
@@ -704,6 +706,23 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab`
 issues.attachment.download = `Click to download "%s"`
 issues.subscribe = Subscribe
 issues.unsubscribe = Unsubscribe
+issues.tracker = Time tracker
+issues.start_tracking_short = Start
+issues.start_tracking = Start time tracking
+issues.start_tracking_history = `started working %s`
+issues.tracking_already_started = `You have already started time tracking on this <a href="%s">issue</a>!`
+issues.stop_tracking = Stop
+issues.stop_tracking_history = `stopped working %s`
+issues.add_time = Add time manually
+issues.add_time_short = Add
+issues.add_time_cancel = Cancel
+issues.add_time_history = `added spent time %s`
+issues.add_time_hours = Hours
+issues.add_time_minutes = Minutes
+issues.add_time_sum_to_small = No time was entered
+issues.cancel_tracking = Cancel
+issues.cancel_tracking_history = `cancelled time tracking %s`
+issues.time_spent_total = Total time spent
 
 pulls.desc = Pulls management your code review and merge requests
 pulls.new = New Pull Request
@@ -818,6 +837,8 @@ settings.tracker_issue_style = External Issue Tracker Naming Style:
 settings.tracker_issue_style.numeric = Numeric
 settings.tracker_issue_style.alphanumeric = Alphanumeric
 settings.tracker_url_format_desc = You can use placeholder <code>{user} {repo} {index}</code> for user name, repository name and issue index.
+settings.enable_timetracker = Enable time tracker
+settings.allow_only_contributors_to_track_time = Allow only contributors to track time
 settings.pulls_desc = Enable pull requests to accept public contributions
 settings.danger_zone = Danger Zone
 settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name.
@@ -1308,6 +1329,8 @@ config.active_code_lives = Active Code Lives
 config.reset_password_code_lives = Reset Password Code Expiry Time
 config.default_keep_email_private = Default Value for Keep Email Private
 config.default_allow_create_organization = Default permission to create organizations
+config.default_enable_timetracking = Enable time tracking by default
+config.default_allow_only_contributors_to_track_time = Allow only contributors to track time by default
 config.no_reply_address = No-reply Address
 
 config.webhook_config = Webhook Configuration
diff --git a/public/js/index.js b/public/js/index.js
index 1b6f2f601e..732beaca0c 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -404,15 +404,19 @@ function initRepository() {
         $('.enable-system').change(function () {
             if (this.checked) {
                 $($(this).data('target')).removeClass('disabled');
+                if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
             } else {
                 $($(this).data('target')).addClass('disabled');
+                if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
             }
         });
         $('.enable-system-radio').change(function () {
             if (this.value == 'false') {
                 $($(this).data('target')).addClass('disabled');
+                if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).removeClass('disabled');
             } else if (this.value == 'true') {
                 $($(this).data('target')).removeClass('disabled');
+                if (typeof $(this).data('context') !== 'undefined')  $($(this).data('context')).addClass('disabled');
             }
         });
     }
@@ -1826,3 +1830,20 @@ function initVueApp() {
         },
     });
 }
+function timeAddManual() {
+    $('.mini.modal')
+        .modal({
+            duration: 200,
+            onApprove: function() {
+                $('#add_time_manual_form').submit();
+            }
+        }).modal('show')
+    ;
+}
+
+function toggleStopwatch() {
+    $("#toggle_stopwatch_form").submit();
+}
+function cancelStopwatch() {
+    $("#cancel_stopwatch_form").submit();
+}
diff --git a/public/swagger.v1.json b/public/swagger.v1.json
index 25ff9fc6be..e640b4e831 100644
--- a/public/swagger.v1.json
+++ b/public/swagger.v1.json
@@ -1372,6 +1372,65 @@
         }
       }
     },
+    "/repos/{username}/{reponame}/issues/{issue}/times": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "operationId": "issueTrackedTimes",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/TrackedTimes"
+          },
+          "404": {
+            "$ref": "#/responses/error"
+          },
+          "500": {
+            "$ref": "#/responses/error"
+          }
+        }
+      },
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "operationId": "addTime",
+        "parameters": [
+          {
+            "x-go-name": "Time",
+            "name": "time",
+            "in": "body",
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/TrackedTime"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "403": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/error"
+          },
+          "500": {
+            "$ref": "#/responses/error"
+          }
+        }
+      }
+    },
     "/repos/{username}/{reponame}/mirror-sync": {
       "post": {
         "produces": [
@@ -1435,6 +1494,53 @@
         }
       }
     },
+    "/repos/{username}/{reponame}/times": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "operationId": "repoTrackedTimes",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/TrackedTimes"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "500": {
+            "$ref": "#/responses/error"
+          }
+        }
+      }
+    },
+    "/repos/{username}/{reponame}/times/{timetrackingusername}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "operationId": "userTrackedTimes",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/TrackedTimes"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/error"
+          },
+          "500": {
+            "$ref": "#/responses/error"
+          }
+        }
+      }
+    },
     "/repositories/{id}": {
       "get": {
         "produces": [
@@ -1951,6 +2057,25 @@
         }
       }
     },
+    "/user/times": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "operationId": "userTrackedTimes",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/TrackedTimes"
+          },
+          "500": {
+            "$ref": "#/responses/error"
+          }
+        }
+      }
+    },
     "/users/:username/followers": {
       "get": {
         "produces": [
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b658c972f1..526940493f 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -350,6 +350,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 					m.Delete("", user.Unstar)
 				}, repoAssignment())
 			})
+			m.Get("/times", repo.ListMyTrackedTimes)
 
 			m.Get("/subscriptions", user.GetMyWatchedRepos)
 		}, reqToken())
@@ -395,6 +396,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 					m.Combo("/:id").Get(repo.GetDeployKey).
 						Delete(repo.DeleteDeploykey)
 				}, reqToken(), reqRepoWriter())
+				m.Group("/times", func() {
+					m.Combo("").Get(repo.ListTrackedTimesByRepository)
+					m.Combo("/:timetrackingusername").Get(repo.ListTrackedTimesByUser)
+
+				}, mustEnableIssues)
 				m.Group("/issues", func() {
 					m.Combo("").Get(repo.ListIssues).
 						Post(reqToken(), bind(api.CreateIssueOption{}), repo.CreateIssue)
@@ -422,6 +428,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 							m.Delete("/:id", reqToken(), repo.DeleteIssueLabel)
 						})
 
+						m.Group("/times", func() {
+							m.Combo("").Get(repo.ListTrackedTimes).
+								Post(reqToken(), bind(api.AddTimeOption{}), repo.AddTime)
+						})
+
 					})
 				}, mustEnableIssues)
 				m.Group("/labels", func() {
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
new file mode 100644
index 0000000000..964fc11ddb
--- /dev/null
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -0,0 +1,158 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	api "code.gitea.io/sdk/gitea"
+)
+
+// ListTrackedTimes list all the tracked times of an issue
+func ListTrackedTimes(ctx *context.APIContext) {
+	// swagger:route GET /repos/{username}/{reponame}/issues/{issue}/times repository issueTrackedTimes
+	//
+	//     Produces:
+	//     - application/json
+	//
+	//     Responses:
+	//       200: TrackedTimes
+	//	 404: error
+	//       500: error
+	if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+		ctx.Error(404, "IsTimetrackerEnabled", "Timetracker is diabled")
+		return
+	}
+	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrIssueNotExist(err) {
+			ctx.Error(404, "GetIssueByIndex", err)
+		} else {
+			ctx.Error(500, "GetIssueByIndex", err)
+		}
+		return
+	}
+
+	if trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+		ctx.Error(500, "GetTrackedTimesByIssue", err)
+	} else {
+		ctx.JSON(200, &trackedTimes)
+	}
+}
+
+// AddTime adds time manual to the given issue
+func AddTime(ctx *context.APIContext, form api.AddTimeOption) {
+	// swagger:route Post /repos/{username}/{reponame}/issues/{issue}/times repository addTime
+	//
+	//     Produces:
+	//     - application/json
+	//
+	//     Responses:
+	//       200: TrackedTime
+	//       400: error
+	//       403: error
+	//	 404: error
+	//       500: error
+	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrIssueNotExist(err) {
+			ctx.Error(404, "GetIssueByIndex", err)
+		} else {
+			ctx.Error(500, "GetIssueByIndex", err)
+		}
+		return
+	}
+
+	if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
+		if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+			ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"})
+			return
+		}
+		ctx.Status(403)
+		return
+	}
+	var tt *models.TrackedTime
+	if tt, err = models.AddTime(ctx.User, issue, form.Time); err != nil {
+		ctx.Error(500, "AddTime", err)
+		return
+	}
+	ctx.JSON(200, tt)
+
+}
+
+// ListTrackedTimesByUser  lists all tracked times of the user
+func ListTrackedTimesByUser(ctx *context.APIContext) {
+	// swagger:route GET /repos/{username}/{reponame}/times/{timetrackingusername} user userTrackedTimes
+	//
+	//     Produces:
+	//     - application/json
+	//
+	//     Responses:
+	//       200: TrackedTimes
+	//       400: error
+	//	 404: error
+	//       500: error
+	if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+		ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"})
+		return
+	}
+	user, err := models.GetUserByName(ctx.Params(":timetrackingusername"))
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			ctx.Error(404, "GetUserByName", err)
+		} else {
+			ctx.Error(500, "GetUserByName", err)
+		}
+		return
+	}
+	if user == nil {
+		ctx.Status(404)
+		return
+	}
+	if trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{UserID: user.ID, RepositoryID: ctx.Repo.Repository.ID}); err != nil {
+		ctx.Error(500, "GetTrackedTimesByUser", err)
+	} else {
+		ctx.JSON(200, &trackedTimes)
+	}
+}
+
+// ListTrackedTimesByRepository lists all tracked times of the user
+func ListTrackedTimesByRepository(ctx *context.APIContext) {
+	// swagger:route GET /repos/{username}/{reponame}/times repository repoTrackedTimes
+	//
+	//     Produces:
+	//     - application/json
+	//
+	//     Responses:
+	//       200: TrackedTimes
+	//       400: error
+	//       500: error
+	if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+		ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"})
+		return
+	}
+	if trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{RepositoryID: ctx.Repo.Repository.ID}); err != nil {
+		ctx.Error(500, "GetTrackedTimesByUser", err)
+	} else {
+		ctx.JSON(200, &trackedTimes)
+	}
+}
+
+// ListMyTrackedTimes lists all tracked times of the current user
+func ListMyTrackedTimes(ctx *context.APIContext) {
+	// swagger:route GET /user/times user userTrackedTimes
+	//
+	//     Produces:
+	//     - application/json
+	//
+	//     Responses:
+	//       200: TrackedTimes
+	//       500: error
+	if trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{UserID: ctx.User.ID}); err != nil {
+		ctx.Error(500, "GetTrackedTimesByUser", err)
+	} else {
+		ctx.JSON(200, &trackedTimes)
+	}
+}
diff --git a/routers/install.go b/routers/install.go
index 08f5d80f3b..427bb960f2 100644
--- a/routers/install.go
+++ b/routers/install.go
@@ -115,6 +115,7 @@ func Install(ctx *context.Context) {
 	form.RequireSignInView = setting.Service.RequireSignInView
 	form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
 	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
+	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
 	form.NoReplyAddress = setting.Service.NoReplyAddress
 
 	auth.AssignForm(form, ctx.Data)
@@ -301,6 +302,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
 	cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(com.ToStr(form.RequireSignInView))
 	cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(com.ToStr(form.DefaultKeepEmailPrivate))
 	cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(com.ToStr(form.DefaultAllowCreateOrganization))
+	cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(com.ToStr(form.DefaultEnableTimetracking))
 	cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(com.ToStr(form.NoReplyAddress))
 
 	cfg.Section("").Key("RUN_MODE").SetValue("prod")
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index d1c5e1fe71..0cd4edabb6 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -589,6 +589,38 @@ func ViewIssue(ctx *context.Context) {
 		comment      *models.Comment
 		participants = make([]*models.User, 1, 10)
 	)
+	if ctx.Repo.Repository.IsTimetrackerEnabled() {
+		if ctx.IsSigned {
+			// Deal with the stopwatch
+			ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
+			if !ctx.Data["IsStopwatchRunning"].(bool) {
+				var exists bool
+				var sw *models.Stopwatch
+				if exists, sw, err = models.HasUserStopwatch(ctx.User.ID); err != nil {
+					ctx.Handle(500, "HasUserStopwatch", err)
+					return
+				}
+				ctx.Data["HasUserStopwatch"] = exists
+				if exists {
+					// Add warning if the user has already a stopwatch
+					var otherIssue *models.Issue
+					if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
+						ctx.Handle(500, "GetIssueByID", err)
+						return
+					}
+					// Add link to the issue of the already running stopwatch
+					ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
+				}
+			}
+			ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
+		} else {
+			ctx.Data["CanUseTimetracker"] = false
+		}
+		if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+			ctx.Handle(500, "TotalTimes", err)
+			return
+		}
+	}
 
 	// Render comments and and fetch participants.
 	participants[0] = issue.Poster
@@ -683,7 +715,8 @@ func ViewIssue(ctx *context.Context) {
 	ctx.HTML(200, tplIssueView)
 }
 
-func getActionIssue(ctx *context.Context) *models.Issue {
+// GetActionIssue will return the issue which is used in the context.
+func GetActionIssue(ctx *context.Context) *models.Issue {
 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 	if err != nil {
 		if models.IsErrIssueNotExist(err) {
@@ -720,7 +753,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
 
 // UpdateIssueTitle change issue's title
 func UpdateIssueTitle(ctx *context.Context) {
-	issue := getActionIssue(ctx)
+	issue := GetActionIssue(ctx)
 	if ctx.Written() {
 		return
 	}
@@ -748,7 +781,7 @@ func UpdateIssueTitle(ctx *context.Context) {
 
 // UpdateIssueContent change issue's content
 func UpdateIssueContent(ctx *context.Context) {
-	issue := getActionIssue(ctx)
+	issue := GetActionIssue(ctx)
 	if ctx.Written() {
 		return
 	}
diff --git a/routers/repo/issue_stopwatch.go b/routers/repo/issue_stopwatch.go
new file mode 100644
index 0000000000..7e3121da9f
--- /dev/null
+++ b/routers/repo/issue_stopwatch.go
@@ -0,0 +1,50 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+)
+
+// IssueStopwatch creates or stops a stopwatch for the given issue.
+func IssueStopwatch(c *context.Context) {
+	issueIndex := c.ParamsInt64("index")
+	issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, issueIndex)
+
+	if err != nil {
+		c.Handle(http.StatusInternalServerError, "GetIssueByIndex", err)
+		return
+	}
+
+	if err := models.CreateOrStopIssueStopwatch(c.User, issue); err != nil {
+		c.Handle(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
+		return
+	}
+
+	url := issue.HTMLURL()
+	c.Redirect(url, http.StatusSeeOther)
+}
+
+// CancelStopwatch cancel the stopwatch
+func CancelStopwatch(c *context.Context) {
+	issueIndex := c.ParamsInt64("index")
+	issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, issueIndex)
+
+	if err != nil {
+		c.Handle(http.StatusInternalServerError, "GetIssueByIndex", err)
+		return
+	}
+
+	if err := models.CancelStopwatch(c.User, issue); err != nil {
+		c.Handle(http.StatusInternalServerError, "CancelStopwatch", err)
+		return
+	}
+
+	url := issue.HTMLURL()
+	c.Redirect(url, http.StatusSeeOther)
+}
diff --git a/routers/repo/issue_timetrack.go b/routers/repo/issue_timetrack.go
new file mode 100644
index 0000000000..e01cd48a66
--- /dev/null
+++ b/routers/repo/issue_timetrack.go
@@ -0,0 +1,50 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"net/http"
+	"time"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth"
+	"code.gitea.io/gitea/modules/context"
+)
+
+// AddTimeManually tracks time manually
+func AddTimeManually(c *context.Context, form auth.AddTimeManuallyForm) {
+	issueIndex := c.ParamsInt64("index")
+	issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, issueIndex)
+	if err != nil {
+		if models.IsErrIssueNotExist(err) {
+			c.Handle(http.StatusNotFound, "GetIssueByIndex", err)
+			return
+		}
+		c.Handle(http.StatusInternalServerError, "GetIssueByIndex", err)
+		return
+	}
+	url := issue.HTMLURL()
+
+	if c.HasError() {
+		c.Flash.Error(c.GetErrMsg())
+		c.Redirect(url)
+		return
+	}
+
+	total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
+
+	if total <= 0 {
+		c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small"))
+		c.Redirect(url, http.StatusSeeOther)
+		return
+	}
+
+	if _, err := models.AddTime(c.User, issue, int64(total)); err != nil {
+		c.Handle(http.StatusInternalServerError, "AddTime", err)
+		return
+	}
+
+	c.Redirect(url, http.StatusSeeOther)
+}
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 8b38e80916..6e12c7ad6d 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -201,7 +201,10 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 					RepoID: repo.ID,
 					Type:   models.UnitTypeIssues,
 					Index:  int(models.UnitTypeIssues),
-					Config: new(models.UnitConfig),
+					Config: &models.IssuesConfig{
+						EnableTimetracker:                form.EnableTimetracker,
+						AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+					},
 				})
 			}
 		}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 5fa93fdb09..938dec1dcf 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -484,6 +484,19 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Post("/content", repo.UpdateIssueContent)
 				m.Post("/watch", repo.IssueWatch)
 				m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
+				m.Group("/times", func() {
+					m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually)
+					m.Group("/stopwatch", func() {
+						m.Post("/toggle", repo.IssueStopwatch)
+						m.Post("/cancel", repo.CancelStopwatch)
+					})
+
+				}, func(ctx *context.Context) {
+					if !ctx.Repo.CanUseTimetracker(repo.GetActionIssue(ctx), ctx.User) {
+						ctx.Handle(404, ctx.Req.RequestURI, nil)
+						return
+					}
+				})
 			})
 
 			m.Post("/labels", repo.UpdateIssueLabel, reqRepoWriter)
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index f9038e1425..b57b1847f6 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -132,6 +132,10 @@
 				<dd><i class="fa fa{{if .Service.DefaultKeepEmailPrivate}}-check{{end}}-square-o"></i></dd>
 				<dt>{{.i18n.Tr "admin.config.default_allow_create_organization"}}</dt>
 				<dd><i class="fa fa{{if .Service.DefaultAllowCreateOrganization}}-check{{end}}-square-o"></i></dd>
+				<dt>{{.i18n.Tr "admin.config.default_enable_timetracking"}}</dt>
+				<dd><i class="fa fa{{if .Service.DefaultEnableTimetracking}}-check{{end}}-square-o"></i></dd>
+				<dt>{{.i18n.Tr "admin.config.default_allow_only_contributors_to_track_time"}}</dt>
+				<dd><i class="fa fa{{if .Service.DefaultAllowOnlyContributorsToTrackTime}}-check{{end}}-square-o"></i></dd>
 				<dt>{{.i18n.Tr "admin.config.no_reply_address"}}</dt>
 				<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
 				<div class="ui divider"></div>
diff --git a/templates/install.tmpl b/templates/install.tmpl
index 5c6d1a9473..924eb2837c 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -230,6 +230,12 @@
 									<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
 								</div>
 							</div>
+							<div class="inline field">
+								<div class="ui checkbox">
+									<label class="poping up" data-content="{{.i18n.Tr "install.default_enable_timetracking_popup"}}"><strong>{{.i18n.Tr "install.default_enable_timetracking"}}</strong></label>
+									<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}>
+								</div>
+							</div>
 							<div class="inline field">
 								<label for="no_reply_address">{{.i18n.Tr "install.no_reply_address"}}</label>
 								<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}">
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index c1b4e7f599..34609ceb31 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -1,7 +1,7 @@
 {{range .Issue.Comments}}
 	{{ $createdStr:= TimeSince .Created $.Lang }}
 
-	<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL -->
+	<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL -->
 	{{if eq .Type 0}}
 		<div class="comment" id="{{.HashTag}}">
 			<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
@@ -58,6 +58,7 @@
 				{{end}}
 			</div>
 		</div>
+
 	{{else if eq .Type 1}}
 		<div class="event">
 			<span class="octicon octicon-primitive-dot"></span>
@@ -140,5 +141,46 @@
 		<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
 		{{$.i18n.Tr "repo.issues.delete_branch_at" .CommitSHA $createdStr | Safe}}
 		</span>
+    {{else if eq .Type 12}}
+		<div class="event">
+			<span class="octicon octicon-primitive-dot"></span>
+			<a class="ui avatar image" href="{{.Poster.HomeLink}}">
+				<img src="{{.Poster.RelAvatarLink}}">
+			</a>
+			<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.start_tracking_history"  $createdStr | Safe}}</span>
+		</div>
+	{{else if eq .Type 13}}
+		<div class="event">
+			<span class="octicon octicon-primitive-dot"></span>
+			<a class="ui avatar image" href="{{.Poster.HomeLink}}">
+				<img src="{{.Poster.RelAvatarLink}}">
+			</a>
+			<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.stop_tracking_history"  $createdStr | Safe}}</span>
+
+			<div class="detail">
+				<span class="octicon octicon-clock"></span>
+				<span class="text grey">{{.Content}}</span>
+			</div>
+		</div>
+	{{else if eq .Type 14}}
+		<div class="event">
+			<span class="octicon octicon-primitive-dot"></span>
+			<a class="ui avatar image" href="{{.Poster.HomeLink}}">
+				<img src="{{.Poster.RelAvatarLink}}">
+			</a>
+			<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.add_time_history"  $createdStr | Safe}}</span>
+			<div class="detail">
+				<span class="octicon octicon-clock"></span>
+				<span class="text grey">{{.Content}}</span>
+			</div>
+		</div>
+	{{else if eq .Type 15}}
+		<div class="event">
+			<span class="octicon octicon-primitive-dot"></span>
+			<a class="ui avatar image" href="{{.Poster.HomeLink}}">
+				<img src="{{.Poster.RelAvatarLink}}">
+			</a>
+			<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> {{$.i18n.Tr "repo.issues.cancel_tracking_history"  $createdStr | Safe}}</span>
+		</div>
 	{{end}}
 {{end}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 2c6149ed29..079c6f52ab 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -102,26 +102,93 @@
 		</div>
 
 		{{if $.IssueWatch}}
-		<div class="ui divider"></div>
+			<div class="ui divider"></div>
 
-		<div class="ui watching">
-			<span class="text"><strong>{{.i18n.Tr "notification.notifications"}}</strong></span>
-			<div>
-				<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/watch">
-					<input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}" />
-					{{$.CsrfTokenHtml}}
-					<button class="fluid ui button">
-						{{if $.IssueWatch.IsWatching}}
-							<i class="octicon octicon-mute"></i>
-							{{.i18n.Tr "repo.issues.unsubscribe"}}
-						{{else}}
-							<i class="octicon octicon-unmute"></i>
-							{{.i18n.Tr "repo.issues.subscribe"}}
-						{{end}}
-					</button>
-				</form>
+			<div class="ui watching">
+				<span class="text"><strong>{{.i18n.Tr "notification.notifications"}}</strong></span>
+				<div>
+					<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/watch">
+						<input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}" />
+						{{$.CsrfTokenHtml}}
+						<button class="fluid ui button">
+							{{if $.IssueWatch.IsWatching}}
+								<i class="octicon octicon-mute"></i>
+								{{.i18n.Tr "repo.issues.unsubscribe"}}
+							{{else}}
+								<i class="octicon octicon-unmute"></i>
+								{{.i18n.Tr "repo.issues.subscribe"}}
+							{{end}}
+						</button>
+					</form>
+				</div>
 			</div>
-		</div>
+		{{end}}
+		{{if .Repository.IsTimetrackerEnabled }}
+			{{if .CanUseTimetracker }}
+				<div class="ui divider"></div>
+				<div class="ui timetrack">
+					<span class="text"><strong>{{.i18n.Tr "repo.issues.tracker"}}</strong></span>
+					<div>
+						<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
+							{{$.CsrfTokenHtml}}
+						</form>
+						<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/times/stopwatch/cancel" id="cancel_stopwatch_form">
+							{{$.CsrfTokenHtml}}
+						</form>
+						{{if  $.IsStopwatchRunning}}
+							<div class="ui buttons fluid stop-cancel">
+								<button onclick="this.disabled=true;toggleStopwatch()" class="ui button stop">{{.i18n.Tr "repo.issues.stop_tracking"}}</button>
+								<button onclick="this.disabled=true;cancelStopwatch()" class="ui negative button cancel">{{.i18n.Tr "repo.issues.cancel_tracking"}}</button>
+							</div>
+						{{else}}
+							{{if .HasUserStopwatch}}
+								<div class="ui warning message">
+									{{.i18n.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL | Safe}}
+								</div>
+							{{end}}
+							<div class="ui buttons two fluid start-add">
+								<button onclick="this.disabled=true;toggleStopwatch()" class="ui button poping up start" data-content='{{.i18n.Tr "repo.issues.start_tracking"}}' data-position="top center" data-variation="small inverted">{{.i18n.Tr "repo.issues.start_tracking_short"}}</button>
+								<button onclick="timeAddManual()" class="ui button green poping up add-time" data-content='{{.i18n.Tr "repo.issues.add_time"}}' data-position="top center" data-variation="small inverted">{{.i18n.Tr "repo.issues.add_time_short"}}</button>
+								<div class="ui mini modal">
+									<div class="header">{{.i18n.Tr "repo.issues.add_time"}}</div>
+									<div class="content">
+										<form method="POST" id="add_time_manual_form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/times/add" class="ui action input fluid">
+											{{$.CsrfTokenHtml}}
+											<input placeholder='{{.i18n.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
+											<input placeholder='{{.i18n.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
+										</form>
+									</div>
+									<div class="actions">
+										<div class="ui green approve button">{{.i18n.Tr "repo.issues.add_time_short"}}</div>
+										<div class="ui red cancel button">{{.i18n.Tr "repo.issues.add_time_cancel"}}</div>
+									</div>
+								</div>
+							</div>
+						{{end}}
+					</div>
+				</div>
+			{{end}}
+			{{if gt (len .WorkingUsers) 0}}
+				<div class="ui divider"></div>
+				<div class="ui participants comments">
+					<span class="text"><strong>{{.i18n.Tr "repo.issues.time_spent_total"}}</strong></span>
+					<div>
+						{{range $user, $trackedtime := .WorkingUsers}}
+							<div class="comment">
+								<a class="avatar">
+									<img src="{{$user.RelAvatarLink}}">
+								</a>
+								<div class="content">
+									<a class="author">{{$user.DisplayName}}</a>
+									<div class="text">
+										{{$trackedtime}}
+									</div>
+								</div>
+							</div>
+						{{end}}
+					</div>
+				</div>
+			{{end}}
 		{{end}}
 	</div>
 </div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 8a7ea218bc..b2b7fc9c3a 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -134,13 +134,28 @@
 				<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
 					<div class="field">
 						<div class="ui radio checkbox">
-							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="false" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled $.UnitTypeExternalTracker)}}checked{{end}}/>
+							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled $.UnitTypeExternalTracker)}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_internal_issue_tracker"}}</label>
 						</div>
 					</div>
+					<div class="field {{if (.Repository.UnitEnabled $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
+						<div class="field">
+							<div class="ui checkbox">
+								<input name="enable_timetracker" class="enable-system" data-target="#only_contributors" type="checkbox" {{if .Repository.IsTimetrackerEnabled}}checked{{end}}>
+								<label>{{.i18n.Tr "repo.settings.enable_timetracker"}}</label>
+							</div>
+						</div>
+						<div class="field {{if not .Repository.IsTimetrackerEnabled}}disabled{{end}}" id="only_contributors">
+							<div class="ui checkbox">
+
+								<input name="allow_only_contributors_to_track_time" type="checkbox" {{if .Repository.AllowOnlyContributorsToTrackTime}}checked{{end}}>
+								<label>{{.i18n.Tr "repo.settings.allow_only_contributors_to_track_time"}}</label>
+							</div>
+						</div>
+					</div>
 					<div class="field">
 						<div class="ui radio checkbox">
-							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="true" data-target="#external_issue_box" {{if .Repository.UnitEnabled $.UnitTypeExternalTracker}}checked{{end}}/>
+							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled $.UnitTypeExternalTracker}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_external_issue_tracker"}}</label>
 						</div>
 					</div>