diff --git a/docs/content/doc/usage/linked-references.en-us.md b/docs/content/doc/usage/linked-references.en-us.md
index d2836f857..bbfd1ef64 100644
--- a/docs/content/doc/usage/linked-references.en-us.md
+++ b/docs/content/doc/usage/linked-references.en-us.md
@@ -42,7 +42,6 @@ Example:
 This is also valid for teams and organizations:
 
 > [@Documenters](#), we need to plan for this.
-
 > [@CoolCompanyInc](#), this issue concerns us all!
 
 Teams will receive mail notifications when appropriate, but whole organizations won't.
@@ -123,6 +122,33 @@ The default _keywords_ are:
 * **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
 * **Reopening**: reopen, reopens, reopened
 
+## Time tracking in Pull Requests and Commit Messages
+
+When commit or merging of pull request results in automatic closing of issue
+it is possible to also add spent time resolving this issue through commit message.
+
+To specify spent time on resolving issue you need to specify time in format
+`@<number><time-unit>` after issue number. In one commit message you can specify
+multiple fixed issues and spent time for each of them.
+
+Supported time units (`<time-unit>`):
+
+* `m` - minutes
+* `h` - hours
+* `d` - days (equals to 8 hours)
+* `w` - weeks (equals to 5 days)
+* `mo` - months (equals to 4 weeks)
+
+Numbers to specify time (`<number>`) can be also decimal numbers, ex. `@1.5h` would
+result in one and half hours. Multiple time units can be combined, ex. `@1h10m` would
+mean 1 hour and 10 minutes.
+
+Example of commit message:
+
+> Fixed #123 spent @1h, refs #102, fixes #124 @1.5h
+
+This would result in 1 hour added to issue #123 and 1 and half hours added to issue #124.
+
 ## External Trackers
 
 Gitea supports the use of external issue trackers, and references to issues
@@ -132,7 +158,6 @@ the pull requests hosted in Gitea. To address this, Gitea allows the use of
 the `!` marker to identify pull requests. For example:
 
 > This is issue [#1234](#), and links to the external tracker.
-
 > This is pull request [!1234](#), and links to a pull request in Gitea.
 
 The `!` and `#` can be used interchangeably for issues and pull request _except_
diff --git a/modules/references/references.go b/modules/references/references.go
index ce08dcc7a..070c6e566 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -37,6 +37,8 @@ var (
 	crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
 	// spaceTrimmedPattern let's us find the trailing space
 	spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`)
+	// timeLogPattern matches string for time tracking
+	timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
 
 	issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
 	issueKeywordsOnce                             sync.Once
@@ -62,10 +64,11 @@ const (
 
 // IssueReference contains an unverified cross-reference to a local issue or pull request
 type IssueReference struct {
-	Index  int64
-	Owner  string
-	Name   string
-	Action XRefAction
+	Index   int64
+	Owner   string
+	Name    string
+	Action  XRefAction
+	TimeLog string
 }
 
 // RenderizableReference contains an unverified cross-reference to with rendering information
@@ -91,16 +94,18 @@ type rawReference struct {
 	issue          string
 	refLocation    *RefSpan
 	actionLocation *RefSpan
+	timeLog        string
 }
 
 func rawToIssueReferenceList(reflist []*rawReference) []IssueReference {
 	refarr := make([]IssueReference, len(reflist))
 	for i, r := range reflist {
 		refarr[i] = IssueReference{
-			Index:  r.index,
-			Owner:  r.owner,
-			Name:   r.name,
-			Action: r.action,
+			Index:   r.index,
+			Owner:   r.owner,
+			Name:    r.name,
+			Action:  r.action,
+			TimeLog: r.timeLog,
 		}
 	}
 	return refarr
@@ -386,6 +391,38 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
 		}
 	}
 
+	if len(ret) == 0 {
+		return ret
+	}
+
+	pos = 0
+
+	for {
+		match := timeLogPattern.FindSubmatchIndex(content[pos:])
+		if match == nil {
+			break
+		}
+
+		timeLogEntry := string(content[match[2]+pos+1 : match[3]+pos])
+
+		var f *rawReference
+		for _, ref := range ret {
+			if ref.refLocation != nil && ref.refLocation.End < match[2]+pos && (f == nil || f.refLocation.End < ref.refLocation.End) {
+				f = ref
+			}
+		}
+
+		pos = match[1] + pos
+
+		if f == nil {
+			f = ret[0]
+		}
+
+		if len(f.timeLog) == 0 {
+			f.timeLog = timeLogEntry
+		}
+	}
+
 	return ret
 }
 
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index 48589c163..0c4037f12 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -26,6 +26,7 @@ type testResult struct {
 	Action         XRefAction
 	RefLocation    *RefSpan
 	ActionLocation *RefSpan
+	TimeLog        string
 }
 
 func TestFindAllIssueReferences(t *testing.T) {
@@ -34,19 +35,19 @@ func TestFindAllIssueReferences(t *testing.T) {
 		{
 			"Simply closes: #29 yes",
 			[]testResult{
-				{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
+				{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
 			},
 		},
 		{
 			"Simply closes: !29 yes",
 			[]testResult{
-				{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
+				{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""},
 			},
 		},
 		{
 			" #124 yes, this is a reference.",
 			[]testResult{
-				{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil},
+				{124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil, ""},
 			},
 		},
 		{
@@ -60,13 +61,13 @@ func TestFindAllIssueReferences(t *testing.T) {
 		{
 			"This user3/repo4#200 yes.",
 			[]testResult{
-				{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
+				{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
 			},
 		},
 		{
 			"This user3/repo4!200 yes.",
 			[]testResult{
-				{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
+				{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
 			},
 		},
 		{
@@ -76,19 +77,19 @@ func TestFindAllIssueReferences(t *testing.T) {
 		{
 			"This [two](/user2/repo1/issues/921) yes.",
 			[]testResult{
-				{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil},
+				{921, "user2", "repo1", "921", false, XRefActionNone, nil, nil, ""},
 			},
 		},
 		{
 			"This [three](/user2/repo1/pulls/922) yes.",
 			[]testResult{
-				{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil},
+				{922, "user2", "repo1", "922", true, XRefActionNone, nil, nil, ""},
 			},
 		},
 		{
 			"This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.",
 			[]testResult{
-				{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil},
+				{203, "user3", "repo4", "203", false, XRefActionNone, nil, nil, ""},
 			},
 		},
 		{
@@ -102,49 +103,49 @@ func TestFindAllIssueReferences(t *testing.T) {
 		{
 			"This http://gitea.com:3000/user4/repo5/pulls/202 yes.",
 			[]testResult{
-				{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil},
+				{202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""},
 			},
 		},
 		{
 			"This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.",
 			[]testResult{
-				{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil},
+				{205, "user4", "repo6", "205", true, XRefActionNone, nil, nil, ""},
 			},
 		},
 		{
 			"Reopens #15 yes",
 			[]testResult{
-				{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}},
+				{15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}, ""},
 			},
 		},
 		{
 			"This closes #20 for you yes",
 			[]testResult{
-				{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}},
+				{20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}, ""},
 			},
 		},
 		{
 			"Do you fix user6/repo6#300 ? yes",
 			[]testResult{
-				{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}},
+				{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}, ""},
 			},
 		},
 		{
 			"For 999 #1235 no keyword, but yes",
 			[]testResult{
-				{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil},
+				{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil, ""},
 			},
 		},
 		{
 			"For [!123] yes",
 			[]testResult{
-				{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
+				{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
 			},
 		},
 		{
 			"For (#345) yes",
 			[]testResult{
-				{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
+				{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""},
 			},
 		},
 		{
@@ -154,31 +155,39 @@ func TestFindAllIssueReferences(t *testing.T) {
 		{
 			"For #24, and #25. yes; also #26; #27? #28! and #29: should",
 			[]testResult{
-				{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil},
-				{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil},
-				{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil},
-				{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil},
-				{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil},
-				{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil},
+				{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil, ""},
+				{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil, ""},
+				{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil, ""},
+				{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil, ""},
+				{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil, ""},
+				{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil, ""},
 			},
 		},
 		{
 			"This user3/repo4#200, yes.",
 			[]testResult{
-				{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
+				{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""},
 			},
 		},
 		{
 			"Which abc. #9434 same as above",
 			[]testResult{
-				{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil},
+				{9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil, ""},
 			},
 		},
 		{
 			"This closes #600 and reopens #599",
 			[]testResult{
-				{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}},
-				{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}},
+				{600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}, ""},
+				{599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}, ""},
+			},
+		},
+		{
+			"This fixes #100 spent @40m and reopens #101, also fixes #102 spent @4h15m",
+			[]testResult{
+				{100, "", "", "100", false, XRefActionCloses, &RefSpan{Start: 11, End: 15}, &RefSpan{Start: 5, End: 10}, "40m"},
+				{101, "", "", "101", false, XRefActionReopens, &RefSpan{Start: 39, End: 43}, &RefSpan{Start: 31, End: 38}, ""},
+				{102, "", "", "102", false, XRefActionCloses, &RefSpan{Start: 56, End: 60}, &RefSpan{Start: 50, End: 55}, "4h15m"},
 			},
 		},
 	}
@@ -237,6 +246,7 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) {
 				issue:          e.Issue,
 				refLocation:    e.RefLocation,
 				actionLocation: e.ActionLocation,
+				timeLog:        e.TimeLog,
 			}
 		}
 		expref := rawToIssueReferenceList(expraw)
@@ -382,25 +392,25 @@ func TestCustomizeCloseKeywords(t *testing.T) {
 		{
 			"Simplemente cierra: #29 yes",
 			[]testResult{
-				{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}},
+				{29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}, ""},
 			},
 		},
 		{
 			"Closes: #123 no, this English.",
 			[]testResult{
-				{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil},
+				{123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil, ""},
 			},
 		},
 		{
 			"CerrĂ³ user6/repo6#300 yes",
 			[]testResult{
-				{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
+				{300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
 			},
 		},
 		{
 			"Reabre user3/repo4#200 yes",
 			[]testResult{
-				{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
+				{200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""},
 			},
 		},
 	}
diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go
index 464249d19..05e9fc958 100644
--- a/modules/repofiles/action.go
+++ b/modules/repofiles/action.go
@@ -8,7 +8,10 @@ import (
 	"encoding/json"
 	"fmt"
 	"html"
+	"regexp"
+	"strconv"
 	"strings"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/git"
@@ -19,6 +22,16 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 )
 
+const (
+	secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute
+	secondsByHour   = 60 * secondsByMinute               // seconds in an hour
+	secondsByDay    = 8 * secondsByHour                  // seconds in a day
+	secondsByWeek   = 5 * secondsByDay                   // seconds in a week
+	secondsByMonth  = 4 * secondsByWeek                  // seconds in a month
+)
+
+var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
+
 // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
 // if the provided ref references a non-existent issue.
 func getIssueFromRef(repo *models.Repository, index int64) (*models.Issue, error) {
@@ -32,6 +45,60 @@ func getIssueFromRef(repo *models.Repository, index int64) (*models.Issue, error
 	return issue, nil
 }
 
+// timeLogToAmount parses time log string and returns amount in seconds
+func timeLogToAmount(str string) int64 {
+	matches := reDuration.FindAllStringSubmatch(str, -1)
+	if len(matches) == 0 {
+		return 0
+	}
+
+	match := matches[0]
+
+	var a int64
+
+	// months
+	if len(match[1]) > 0 {
+		mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64)
+		a += int64(mo * secondsByMonth)
+	}
+
+	// weeks
+	if len(match[3]) > 0 {
+		w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64)
+		a += int64(w * secondsByWeek)
+	}
+
+	// days
+	if len(match[5]) > 0 {
+		d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64)
+		a += int64(d * secondsByDay)
+	}
+
+	// hours
+	if len(match[7]) > 0 {
+		h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64)
+		a += int64(h * secondsByHour)
+	}
+
+	// minutes
+	if len(match[9]) > 0 {
+		d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64)
+		a += int64(d * secondsByMinute)
+	}
+
+	return a
+}
+
+func issueAddTime(issue *models.Issue, doer *models.User, time time.Time, timeLog string) error {
+	amount := timeLogToAmount(timeLog)
+	if amount == 0 {
+		return nil
+	}
+
+	_, err := models.AddTime(doer, issue, amount, time)
+	return err
+}
+
 func changeIssueStatus(repo *models.Repository, issue *models.Issue, doer *models.User, closed bool) error {
 	stopTimerIfAvailable := func(doer *models.User, issue *models.Issue) error {
 
@@ -139,6 +206,11 @@ func UpdateIssuesCommit(doer *models.User, repo *models.Repository, commits []*r
 				}
 			}
 			close := (ref.Action == references.XRefActionCloses)
+			if close && len(ref.TimeLog) > 0 {
+				if err := issueAddTime(refIssue, doer, c.Timestamp, ref.TimeLog); err != nil {
+					return err
+				}
+			}
 			if close != refIssue.IsClosed {
 				if err := changeIssueStatus(refRepo, refIssue, doer, close); err != nil {
 					return err