diff --git a/docs/content/doc/installation/with-docker.en-us.md b/docs/content/doc/installation/with-docker.en-us.md index 563e85c226534..b8017e64dee9e 100644 --- a/docs/content/doc/installation/with-docker.en-us.md +++ b/docs/content/doc/installation/with-docker.en-us.md @@ -345,19 +345,23 @@ ports: - "127.0.0.1:2222:22" ``` -In addition, `/home/git/.ssh/authorized_keys` on the host needs to be modified. It needs to act in the same way as `authorized_keys` within the Gitea container. Therefore add +In addition, `/home/git/.ssh/authorized_keys` on the host needs to be modified. It needs to act in the same way as `authorized_keys` within the Gitea container. Therefore add the public key of the key you created above ("Gitea Host Key") to `~/git/.ssh/authorized_keys`. +This can be done via `echo "$(cat /home/git/.ssh/id_rsa.pub)" >> /home/git/.ssh/authorized_keys`. +Important: The pubkey from the `git` user needs to be added "as is" while all other pubkeys added via the Gitea web interface will be prefixed with `command="/app [...]`. -```bash -command="/app/gitea/gitea --config=/data/gitea/conf/app.ini serv key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa -``` +The file should then look somewhat like -and replace `` with a valid SSH public key of yours. +```bash +# SSH pubkey from git user +ssh-rsa -In addition the public key of the `git` user on the host needs to be added to `/home/git/.ssh/authorized_keys` so authentication against the container can succeed: `echo "$(cat /home/git/.ssh/id_rsa.pub)" >> /home/git/.ssh/authorized_keys`. +# other keys from users +command="/app/gitea/gitea --config=/data/gitea/conf/app.ini serv key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty +``` Here is a detailed explanation what is happening when a SSH request is made: -1. A SSH request is made against the host using the `git` user, e.g. `git clone git@domain:user/repo.git`. +1. A SSH request is made against the host (usually port 22) using the `git` user, e.g. `git clone git@domain:user/repo.git`. 2. In `/home/git/.ssh/authorized_keys` , the command executes the `/app/gitea/gitea` script. 3. `/app/gitea/gitea` forwards the SSH request to port 2222 which is mapped to the SSH port (22) of the container. 4. Due to the existence of the public key of the `git` user in `/home/git/.ssh/authorized_keys` the authentication host → container succeeds and the SSH request get forwarded to Gitea running in the docker container. diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go index 39b9b97411dee..c0b8fd9c69746 100644 --- a/integrations/api_issue_stopwatch_test.go +++ b/integrations/api_issue_stopwatch_test.go @@ -7,7 +7,6 @@ package integrations import ( "net/http" "testing" - "time" "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" @@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) { issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) if assert.Len(t, apiWatches, 1) { assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) - apiWatches[0].Created = time.Time{} - assert.EqualValues(t, api.StopWatch{ - Created: time.Time{}, - IssueIndex: issue.Index, - IssueTitle: issue.Title, - RepoName: repo.Name, - RepoOwnerName: repo.OwnerName, - }, *apiWatches[0]) + assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) + assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) + assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) + assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) + assert.Greater(t, int64(apiWatches[0].Seconds), int64(0)) } } diff --git a/integrations/attachment_test.go b/integrations/attachment_test.go index dd734145d2b9f..a28e38b9907cf 100644 --- a/integrations/attachment_test.go +++ b/integrations/attachment_test.go @@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("form").Attr("action") + link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action") assert.True(t, exists, "The template has changed") postData := map[string]string{ diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 4b2bf1505d4db..a1c88503d8990 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -19,6 +19,16 @@ type Stopwatch struct { CreatedUnix timeutil.TimeStamp `xorm:"created"` } +// Seconds returns the amount of time passed since creation, based on local server time +func (s Stopwatch) Seconds() int64 { + return int64(timeutil.TimeStampNow() - s.CreatedUnix) +} + +// Duration returns a human-readable duration string based on local server time +func (s Stopwatch) Duration() string { + return SecToTime(s.Seconds()) +} + func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = e. diff --git a/modules/convert/issue.go b/modules/convert/issue.go index 36446da2d160e..b773e78a6b5cc 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), + Seconds: sw.Seconds(), + Duration: sw.Duration(), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/modules/structs/issue_stopwatch.go b/modules/structs/issue_stopwatch.go index 8599e072731f5..15d17cdda7e34 100644 --- a/modules/structs/issue_stopwatch.go +++ b/modules/structs/issue_stopwatch.go @@ -12,6 +12,8 @@ import ( type StopWatch struct { // swagger:strfmt date-time Created time.Time `json:"created"` + Seconds int64 `json:"seconds"` + Duration string `json:"duration"` IssueIndex int64 `json:"issue_index"` IssueTitle string `json:"issue_title"` RepoOwnerName string `json:"repo_owner_name"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 142f3049f34ca..30fa5f8a731e9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -15,6 +15,7 @@ page = Page template = Template language = Language notifications = Notifications +active_stopwatch = Active Time Tracker create_new = Create… user_profile_and_more = Profile and Settings… signed_in_as = Signed in as @@ -1068,6 +1069,7 @@ issues.commented_at = `commented %s` issues.delete_comment_confirm = Are you sure you want to delete this comment? issues.context.copy_link = Copy Link issues.context.quote_reply = Quote Reply +issues.context.reference_issue = Reference in new issue issues.context.edit = Edit issues.context.delete = Delete issues.no_content = There is no content yet. @@ -1138,13 +1140,15 @@ issues.lock.title = Lock conversation on this issue. issues.unlock.title = Unlock conversation on this issue. issues.comment_on_locked = You cannot comment on a locked issue. issues.tracker = Time Tracker -issues.start_tracking_short = Start +issues.start_tracking_short = Start Timer issues.start_tracking = Start Time Tracking issues.start_tracking_history = `started working %s` issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed issues.tracking_already_started = `You have already started time tracking on another issue!` -issues.stop_tracking = Stop +issues.stop_tracking = Stop Timer issues.stop_tracking_history = `stopped working %s` +issues.cancel_tracking = Discard +issues.cancel_tracking_history = `cancelled time tracking %s` issues.add_time = Manually Add Time issues.add_time_short = Add Time issues.add_time_cancel = Cancel @@ -1153,8 +1157,6 @@ issues.del_time_history= `deleted 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 issues.time_spent_from_all_authors = `Total Time Spent: %s` issues.due_date = Due Date @@ -1225,6 +1227,7 @@ issues.review.resolve_conversation = Resolve conversation issues.review.un_resolve_conversation = Unresolve conversation issues.review.resolved_by = marked this conversation as resolved issues.assignee.error = Not all assignees was added due to an unexpected error. +issues.reference_issue.body = Body pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request diff --git a/package-lock.json b/package-lock.json index 3fe85f8c64c56..12f63fb81331c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5293,6 +5293,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" + }, "parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -6702,6 +6707,14 @@ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "optional": true }, + "pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "requires": { + "parse-ms": "^2.1.0" + } + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", diff --git a/package.json b/package.json index 2abdc5ab7e5a5..8252376643aed 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "monaco-editor": "0.21.2", "monaco-editor-webpack-plugin": "2.1.0", "postcss": "8.2.1", + "pretty-ms": "7.0.1", "raw-loader": "4.0.2", "sortablejs": "1.12.0", "swagger-ui-dist": "3.38.0", diff --git a/routers/repo/issue_stopwatch.go b/routers/repo/issue_stopwatch.go index 28105dfe03152..b8efb3b841363 100644 --- a/routers/repo/issue_stopwatch.go +++ b/routers/repo/issue_stopwatch.go @@ -6,6 +6,7 @@ package repo import ( "net/http" + "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) { url := issue.HTMLURL() c.Redirect(url, http.StatusSeeOther) } + +// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context +func GetActiveStopwatch(c *context.Context) { + if strings.HasPrefix(c.Req.URL.Path, "/api") { + return + } + + if !c.IsSigned { + return + } + + _, sw, err := models.HasUserStopwatch(c.User.ID) + if err != nil { + c.ServerError("HasUserStopwatch", err) + return + } + + if sw == nil || sw.ID == 0 { + return + } + + issue, err := models.GetIssueByID(sw.IssueID) + if err != nil || issue == nil { + c.ServerError("GetIssueByID", err) + return + } + if err = issue.LoadRepo(); err != nil { + c.ServerError("LoadRepo", err) + return + } + + c.Data["ActiveStopwatch"] = StopwatchTmplInfo{ + issue.Repo.FullName(), + issue.Index, + sw.Seconds() + 1, // ensure time is never zero in ui + } +} + +// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering +type StopwatchTmplInfo struct { + RepoSlug string + IssueIndex int64 + Seconds int64 +} diff --git a/routers/routes/macaron.go b/routers/routes/macaron.go index 34978724a8368..f64a0a597b584 100644 --- a/routers/routes/macaron.go +++ b/routers/routes/macaron.go @@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { } m.Use(user.GetNotificationCount) + m.Use(repo.GetActiveStopwatch) m.Use(func(ctx *context.Context) { ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled() ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled() diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index a2b4d4f1d9fc8..efab76f33c0f4 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -67,6 +67,44 @@ {{else if .IsSigned}} {{end}} + {{template "repo/issue/view_content/reference_issue_dialog" .}} + {{if .IsSplitStyle}}