-
-
- {{template "repo/migrate/helper" .}} - diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 1ea22660f7..e9fbb67313 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -13,43 +13,46 @@ Template Attributes: * EasyMDE: whether to display button for switching to legacy editor */}}
- {{if .MarkdownPreviewUrl}} - - {{end}} + + + {{if .MarkdownPreviewUrl}} + + {{end}} +
+ {{svg "octicon-heading"}} + {{svg "octicon-bold"}} + {{svg "octicon-italic"}} +
+
+ {{svg "octicon-quote"}} + {{svg "octicon-code"}} + +
+
+ {{svg "octicon-list-unordered"}} + {{svg "octicon-list-ordered"}} + {{svg "octicon-tasklist"}} + + +
+
+ + {{svg "octicon-mention"}} + {{svg "octicon-cross-reference"}} +
+
+ + {{if .EasyMDE}} + + {{end}} +
+
- -
- {{svg "octicon-heading"}} - {{svg "octicon-bold"}} - {{svg "octicon-italic"}} -
-
- {{svg "octicon-quote"}} - {{svg "octicon-code"}} - -
-
- {{svg "octicon-list-unordered"}} - {{svg "octicon-list-ordered"}} - {{svg "octicon-tasklist"}} - - -
-
- - {{svg "octicon-mention"}} - {{svg "octicon-cross-reference"}} -
-
- - {{if .EasyMDE}} - - {{end}} -
-
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 53575d82b2..6c0950caff 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -52,25 +52,27 @@
{{end}}
-
- - {{if eq $.listType "dashboard"}} - {{.Repo.FullName}}#{{.Index}} +
+ + + {{if eq $.listType "dashboard"}} + {{.Repo.FullName}}#{{.Index}} + {{else}} + #{{.Index}} + {{end}} + + {{$timeStr := DateUtils.TimeSince .GetLastEventTimestamp}} + {{if .OriginalAuthor}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} + {{else if gt .Poster.ID 0}} + {{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}} {{else}} - #{{.Index}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}} - - {{$timeStr := DateUtils.TimeSince .GetLastEventTimestamp}} - {{if .OriginalAuthor}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} - {{else if gt .Poster.ID 0}} - {{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}} - {{else}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} - {{end}} + {{if .IsPull}} -
- {{svg "gitea-double-chevron-right" 12}} + {{svg "gitea-double-chevron-right" 12}} +
{{/* inline to remove the spaces between spans */}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a16deb61a8..d1571d1b13 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15630,6 +15630,172 @@ } } }, + "/repos/{owner}/{repo}/sync_fork": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets information about syncing the fork default branch with the base branch", + "operationId": "repoSyncForkDefaultInfo", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/SyncForkInfo" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Syncs the default branch of a fork with the base branch", + "operationId": "repoSyncForkDefault", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/sync_fork/{branch}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets information about syncing a fork branch with the base branch", + "operationId": "repoSyncForkBranchInfo", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The branch", + "name": "branch", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/SyncForkInfo" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Syncs a fork branch with the base branch", + "operationId": "repoSyncForkBranch", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The branch", + "name": "branch", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/tag_protections": { "get": { "produces": [ @@ -17149,6 +17315,29 @@ } } }, + "/signing-key.ssh": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Get default signing-key.ssh", + "operationId": "getSSHSigningKey", + "responses": { + "200": { + "description": "SSH public key in OpenSSH authorized key format", + "schema": { + "type": "string" + } + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/teams/{id}": { "get": { "produces": [ @@ -21833,6 +22022,11 @@ "type": "string", "x-go-name": "LastCommitSHA" }, + "last_commit_when": { + "type": "string", + "format": "date-time", + "x-go-name": "LastCommitWhen" + }, "name": { "type": "string", "x-go-name": "Name" @@ -27432,6 +27626,30 @@ }, "x-go-package": "forgejo.org/modules/structs" }, + "SyncForkInfo": { + "description": "SyncForkInfo information about syncing a fork", + "type": "object", + "properties": { + "allowed": { + "type": "boolean", + "x-go-name": "Allowed" + }, + "base_commit": { + "type": "string", + "x-go-name": "BaseCommit" + }, + "commits_behind": { + "type": "integer", + "format": "int64", + "x-go-name": "CommitsBehind" + }, + "fork_commit": { + "type": "string", + "x-go-name": "ForkCommit" + } + }, + "x-go-package": "forgejo.org/modules/structs" + }, "Tag": { "description": "Tag represents a repository tag", "type": "object", @@ -29211,6 +29429,15 @@ } } }, + "SyncForkInfo": { + "description": "SyncForkInfo", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/SyncForkInfo" + } + } + }, "Tag": { "description": "Tag", "schema": { diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index e25a9d8219..c76a61c393 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -6,11 +6,11 @@
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index b269c63b37..267d5e3534 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -37,11 +37,11 @@
@@ -114,7 +114,7 @@ {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .DeadlineString}} - + {{svg "octicon-calendar" 14}} {{DateUtils.AbsoluteShort (.DeadlineString|DateUtils.ParseLegacy)}} diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl index b8783dead0..420a25cb1d 100644 --- a/templates/user/settings/keys_ssh.tmpl +++ b/templates/user/settings/keys_ssh.tmpl @@ -78,15 +78,16 @@

{{ctx.Locale.Tr "settings.ssh_token_help"}}

-

echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

+
bash -c "echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n gitea -f <(echo '{{.OmitEmail}}')"
+
Windows PowerShell -

cmd /c "<NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey"

+
cmd /c "<NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey"

Windows CMD -

set /p={{$.TokenToSign}}| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

+
set /p={{$.TokenToSign}}| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey

diff --git a/templates/user/settings/packages.tmpl b/templates/user/settings/packages.tmpl index bd7d69b259..bdfdac8e37 100644 --- a/templates/user/settings/packages.tmpl +++ b/templates/user/settings/packages.tmpl @@ -9,14 +9,14 @@
- +
{{.CsrfTokenHtml}}
- +
diff --git a/tests/e2e/declare_repos_test.go b/tests/e2e/declare_repos_test.go index 774b587575..2e6e8e7361 100644 --- a/tests/e2e/declare_repos_test.go +++ b/tests/e2e/declare_repos_test.go @@ -13,6 +13,7 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/git" + "forgejo.org/modules/indexer/stats" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" @@ -36,6 +37,10 @@ func DeclareGitRepos(t *testing.T) func() { Filename: "testfile", Versions: []string{"hello", "hallo", "hola", "native", "ubuntu-latest", "- runs-on: ubuntu-latest", "- runs-on: debian-latest"}, }}), + newRepo(t, 2, "language-stats-test", []FileChanges{{ + Filename: "main.rs", + Versions: []string{"fn main() {", "println!(\"Hello World!\");", "}"}, + }}), newRepo(t, 2, "mentions-highlighted", []FileChanges{ { Filename: "history1.md", @@ -109,5 +114,8 @@ func newRepo(t *testing.T, userID int64, repoName string, fileChanges []FileChan } } + err := stats.UpdateRepoIndexer(somerepo) + require.NoError(t, err) + return cleanupFunc } diff --git a/tests/e2e/dimmer.test.e2e.ts b/tests/e2e/dimmer.test.e2e.ts index 04c6433a5a..9ee6f82c07 100644 --- a/tests/e2e/dimmer.test.e2e.ts +++ b/tests/e2e/dimmer.test.e2e.ts @@ -37,3 +37,36 @@ test('Dimmed modal', async ({page}) => { await expect(page.locator('.ui.dimmer')).toHaveCount(1); await save_visual(page); }); + +test('Dimmed overflow', async ({page}, workerInfo) => { + test.skip(['Mobile Safari'].includes(workerInfo.project.name), 'Mouse wheel is not supported in mobile WebKit'); + await page.goto('/user2/repo1/_new/master/'); + + // Type in a file name. + await page.locator('#file-name').click(); + await page.keyboard.type('todo.txt'); + + // Scroll to the bottom. + const scrollY = await page.evaluate(() => document.body.scrollHeight); + await page.mouse.wheel(0, scrollY); + + // Click on 'Commit changes' + await page.locator('#commit-button').click(); + + // Expect a 'are you sure, this file is empty' modal. + await expect(page.locator('.ui.dimmer')).toBeVisible(); + await expect(page.locator('.ui.dimmer .header')).toContainText('Commit an empty file'); + await save_visual(page); + + // Trickery to check that the dimmer covers the whole page. + const viewport = page.viewportSize(); + const box = await page.locator('.ui.dimmer').boundingBox(); + expect(box.x).toBe(0); + expect(box.y).toBe(0); + expect(box.width).toBe(viewport.width); + expect(box.height).toBe(viewport.height); + + // Trickery to check the page cannot be scrolled. + const {scrollHeight, clientHeight} = await page.evaluate(() => document.body); + expect(scrollHeight).toBe(clientHeight); +}); diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index 39ced25bc3..c69c9a7f0c 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -39,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => { await save_visual(page); }); -test('markdown indentation', async ({page}) => { +test('Markdown indentation', async ({page}) => { const initText = `* first\n* second\n* third\n* last`; const response = await page.goto('/user2/repo1/issues/new'); @@ -109,7 +109,7 @@ test('markdown indentation', async ({page}) => { await expect(textarea).toHaveValue(initText); }); -test('markdown list continuation', async ({page}) => { +test('Markdown list continuation', async ({page}) => { const initText = `* first\n* second`; const response = await page.goto('/user2/repo1/issues/new'); @@ -202,7 +202,7 @@ test('markdown list continuation', async ({page}) => { } }); -test('markdown insert table', async ({page}) => { +test('Markdown insert table', async ({page}) => { const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -225,7 +225,7 @@ test('markdown insert table', async ({page}) => { await save_visual(page); }); -test('markdown insert link', async ({page}) => { +test('Markdown insert link', async ({page}) => { const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -277,3 +277,43 @@ test('text expander has higher prio then prefix continuation', async ({page}) => await textarea.press('Enter'); await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `); }); + +test('Combo Markdown: preview mode switch', async ({page}) => { + // Load page with editor + const response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + const toolbarItem = page.locator('md-header'); + const editorPanel = page.locator('[data-tab-panel="markdown-writer"]'); + const previewPanel = page.locator('[data-tab-panel="markdown-previewer"]'); + + // Verify correct visibility of related UI elements + await expect(toolbarItem).toBeVisible(); + await expect(editorPanel).toBeVisible(); + await expect(previewPanel).toBeHidden(); + + // Fill some content + const textarea = page.locator('textarea.markdown-text-editor'); + await textarea.fill('**Content** :100: _100_'); + + // Switch to preview mode + await page.locator('a[data-tab-for="markdown-previewer"]').click(); + + // Verify that the related UI elements were switched correctly + await expect(toolbarItem).toBeHidden(); + await expect(editorPanel).toBeHidden(); + await expect(previewPanel).toBeVisible(); + await save_visual(page); + + // Verify that some content rendered + await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible(); + + // Switch back to edit mode + await page.locator('a[data-tab-for="markdown-writer"]').click(); + + // Verify that the related UI elements were switched back correctly + await expect(toolbarItem).toBeVisible(); + await expect(editorPanel).toBeVisible(); + await expect(previewPanel).toBeHidden(); + await save_visual(page); +}); diff --git a/tests/e2e/repo-home.e2e.ts b/tests/e2e/repo-home.e2e.ts deleted file mode 100644 index fbcfe17226..0000000000 --- a/tests/e2e/repo-home.e2e.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @watch start -// web_src/js/features/common-global.js -// web_src/css/repo.css -// @watch end - -import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; - -test('Language stats bar', async ({page}) => { - const response = await page.goto('/user2/repo1'); - expect(response?.status()).toBe(200); - - await expect(page.locator('#language-stats-legend')).toBeVisible(); - await save_visual(page); - - await page.click('#language-stats-bar'); - await expect(page.locator('#language-stats-legend')).toBeHidden(); - await save_visual(page); -}); diff --git a/tests/e2e/repo-home.test.e2e.ts b/tests/e2e/repo-home.test.e2e.ts new file mode 100644 index 0000000000..6f3d6c373b --- /dev/null +++ b/tests/e2e/repo-home.test.e2e.ts @@ -0,0 +1,35 @@ +// @watch start +// web_src/js/components/RepoBranchTagSelector.vue +// web_src/js/features/common-global.js +// web_src/css/repo.css +// @watch end + +import {expect} from '@playwright/test'; +import {save_visual, test} from './utils_e2e.ts'; + +test('Language stats bar', async ({page}) => { + const response = await page.goto('/user2/language-stats-test'); + expect(response?.status()).toBe(200); + + await expect(page.locator('#language-stats-legend')).toBeHidden(); + + await page.click('#language-stats-bar'); + await expect(page.locator('#language-stats-legend')).toBeVisible(); + await save_visual(page); + + await page.click('#language-stats-bar'); + await expect(page.locator('#language-stats-legend')).toBeHidden(); + await save_visual(page); +}); + +test('Branch selector commit icon', async ({page}) => { + const response = await page.goto('/user2/repo1'); + expect(response?.status()).toBe(200); + + await expect(page.locator('.branch-dropdown-button svg.octicon-git-branch')).toBeVisible(); + await expect(page.locator('.branch-dropdown-button')).toHaveText('master'); + + await page.goto('/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d'); + await expect(page.locator('.branch-dropdown-button svg.octicon-git-commit')).toBeVisible(); + await expect(page.locator('.branch-dropdown-button')).toHaveText('65f1bf27bc'); +}); diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts index b6541179f0..f0be73e777 100644 --- a/tests/e2e/repo-migrate.test.e2e.ts +++ b/tests/e2e/repo-migrate.test.e2e.ts @@ -7,6 +7,24 @@ import {test, save_visual, test_context, dynamic_id} from './utils_e2e.ts'; test.use({user: 'user2'}); +test('Migration type seleciton screen', async ({page}) => { + await page.goto('/repo/migrate'); + + // For branding purposes, it is desired that `gitea-` prefixes in SVGs are + // replaced with something like `productlogo-`. + await expect(page.locator('svg.gitea-git')).toBeVisible(); + await expect(page.locator('svg.octicon-mark-github')).toBeVisible(); + await expect(page.locator('svg.gitea-gitlab')).toBeVisible(); + await expect(page.locator('svg.gitea-forgejo')).toBeVisible(); + await expect(page.locator('svg.gitea-gitea')).toBeVisible(); + await expect(page.locator('svg.gitea-gogs')).toBeVisible(); + await expect(page.locator('svg.gitea-onedev')).toBeVisible(); + await expect(page.locator('svg.gitea-gitbucket')).toBeVisible(); + await expect(page.locator('svg.gitea-codebase')).toBeVisible(); + + await save_visual(page); +}); + test('Migration Repo Name detection', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky actionability checks on Mobile Safari'); diff --git a/tests/integration/api_federation_httpsig_test.go b/tests/integration/api_federation_httpsig_test.go index 9d66f25102..a7a5ae26ed 100644 --- a/tests/integration/api_federation_httpsig_test.go +++ b/tests/integration/api_federation_httpsig_test.go @@ -64,7 +64,7 @@ func TestFederationHttpSigValidation(t *testing.T) { assert.NotNil(t, host) assert.True(t, host.PublicKey.Valid) - user, err := user.GetFederatedUserByKeyID(db.DefaultContext, actorKeyID) + _, user, err := user.FindFederatedUserByKeyID(db.DefaultContext, actorKeyID) require.NoError(t, err) assert.NotNil(t, user) assert.True(t, user.PublicKey.Valid) diff --git a/tests/integration/api_misc_test.go b/tests/integration/api_misc_test.go new file mode 100644 index 0000000000..2687a9bb31 --- /dev/null +++ b/tests/integration/api_misc_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/http" + "testing" + + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestAPISSHSigningKey(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("No signing key", func(t *testing.T) { + defer test.MockVariableValue(&setting.SSHInstanceKey, nil)() + defer tests.PrintCurrentTest(t)() + + MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusNotFound) + }) + t.Run("With signing key", func(t *testing.T) { + publicKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n" + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey)) + require.NoError(t, err) + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() + defer tests.PrintCurrentTest(t)() + + resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusOK) + assert.Equal(t, publicKey, resp.Body.String()) + }) +} diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index 2cc05c42bb..4d34e5d43f 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -177,6 +177,8 @@ func TestAPICreateFile(t *testing.T) { expectedFileResponse := getExpectedFileResponseForCreate("user2/repo1", commitID, treePath, latestCommit.ID.String()) var fileResponse api.FileResponse DecodeJSON(t, resp, &fileResponse) + // Testify cannot assert time.Time correctly. + expectedFileResponse.Content.LastCommitWhen = fileResponse.Content.LastCommitWhen assert.Equal(t, expectedFileResponse.Content, fileResponse.Content) assert.Equal(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) assert.Equal(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) @@ -296,6 +298,8 @@ func TestAPICreateFile(t *testing.T) { latestCommit, _ := gitRepo.GetCommitByPath(treePath) expectedFileResponse := getExpectedFileResponseForCreate("user2/"+reponame, commitID, treePath, latestCommit.ID.String()) DecodeJSON(t, resp, &fileResponse) + // Testify cannot assert time.Time correctly. + expectedFileResponse.Content.LastCommitWhen = fileResponse.Content.LastCommitWhen assert.Equal(t, expectedFileResponse.Content, fileResponse.Content) assert.Equal(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) assert.Equal(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index b14dfbc565..098d7a3fde 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -140,6 +140,8 @@ func TestAPIUpdateFile(t *testing.T) { expectedFileResponse := getExpectedFileResponseForUpdate(commitID, treePath, lasCommit.ID.String()) var fileResponse api.FileResponse DecodeJSON(t, resp, &fileResponse) + // Testify cannot assert time.Time correctly. + expectedFileResponse.Content.LastCommitWhen = fileResponse.Content.LastCommitWhen assert.Equal(t, expectedFileResponse.Content, fileResponse.Content) assert.Equal(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) assert.Equal(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go index 1272c3d8bf..1772dec6a6 100644 --- a/tests/integration/api_repo_files_change_test.go +++ b/tests/integration/api_repo_files_change_test.go @@ -104,6 +104,10 @@ func TestAPIChangeFiles(t *testing.T) { var filesResponse api.FilesResponse DecodeJSON(t, resp, &filesResponse) + // Testify cannot assert time.Time correctly. + expectedCreateFileResponse.Content.LastCommitWhen = filesResponse.Files[0].LastCommitWhen + expectedUpdateFileResponse.Content.LastCommitWhen = filesResponse.Files[1].LastCommitWhen + // check create file assert.Equal(t, expectedCreateFileResponse.Content, filesResponse.Files[0]) diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go index 2073c2cb98..7d010ffdf1 100644 --- a/tests/integration/api_repo_get_contents_list_test.go +++ b/tests/integration/api_repo_get_contents_list_test.go @@ -8,6 +8,7 @@ import ( "net/url" "path/filepath" "testing" + "time" auth_model "forgejo.org/models/auth" repo_model "forgejo.org/models/repo" @@ -32,16 +33,17 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath return []*api.ContentsResponse{ { - Name: filepath.Base(treePath), - Path: treePath, - SHA: sha, - LastCommitSHA: lastCommitSHA, - Type: "file", - Size: 30, - URL: &selfURL, - HTMLURL: &htmlURL, - GitURL: &gitURL, - DownloadURL: &downloadURL, + Name: filepath.Base(treePath), + Path: treePath, + SHA: sha, + LastCommitSHA: lastCommitSHA, + LastCommitWhen: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), + Type: "file", + Size: 30, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, Links: &api.FileLinksResponse{ Self: &selfURL, GitURL: &gitURL, diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go index 4053828082..47ac4cfc03 100644 --- a/tests/integration/api_repo_get_contents_test.go +++ b/tests/integration/api_repo_get_contents_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "testing" + "time" auth_model "forgejo.org/models/auth" repo_model "forgejo.org/models/repo" @@ -33,18 +34,19 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath return &api.ContentsResponse{ - Name: treePath, - Path: treePath, - SHA: sha, - LastCommitSHA: lastCommitSHA, - Type: "file", - Size: 30, - Encoding: &encoding, - Content: &content, - URL: &selfURL, - HTMLURL: &htmlURL, - GitURL: &gitURL, - DownloadURL: &downloadURL, + Name: treePath, + Path: treePath, + SHA: sha, + LastCommitSHA: lastCommitSHA, + LastCommitWhen: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), + Type: "file", + Size: 30, + Encoding: &encoding, + Content: &content, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, Links: &api.FileLinksResponse{ Self: &selfURL, GitURL: &gitURL, diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go deleted file mode 100644 index 271c225a3f..0000000000 --- a/tests/integration/gpg_git_test.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package integration - -import ( - "encoding/base64" - "fmt" - "net/url" - "os" - "testing" - - auth_model "forgejo.org/models/auth" - "forgejo.org/models/unittest" - user_model "forgejo.org/models/user" - "forgejo.org/modules/git" - "forgejo.org/modules/process" - "forgejo.org/modules/setting" - api "forgejo.org/modules/structs" - "forgejo.org/modules/test" - "forgejo.org/tests" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGPGGit(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring - err := os.Chmod(tmpDir, 0o700) - require.NoError(t, err) - - t.Setenv("GNUPGHOME", tmpDir) - require.NoError(t, err) - - // Need to create a root key - rootKeyPair, err := importTestingKey() - require.NoError(t, err, "importTestingKey") - - defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() - defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() - defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() - defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() - defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() - - username := "user2" - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) - baseAPITestContext := NewAPITestContext(t, username, "repo1") - - onGiteaRun(t, func(t *testing.T, u *url.URL) { - u.Path = baseAPITestContext.GitPath() - - t.Run("Unsigned-Initial", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"never"} - t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"always"} - t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Always", crudActionCreateFile( - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( - t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.InitialCommit = []string{"always"} - t.Run("AlwaysSign-Initial", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - if branch.Commit == nil { - assert.FailNow(t, "no commit provided with branch", "branch: %v", branch) - } - assert.NotNil(t, branch.Commit.Verification) - if branch.Commit.Verification == nil { - assert.FailNow(t, "no verification provided with branch commit", "commit: %v", branch.Commit) - } - assert.True(t, branch.Commit.Verification.Verified) - if !branch.Commit.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"never"} - t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - return - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"always"} - t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-Always", crudActionCreateFile( - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - return - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.Merges = []string{"commitssigned"} - t.Run("UnsignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - }) - - setting.Repository.Signing.Merges = []string{"basesigned"} - t.Run("BaseSignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - }) - - setting.Repository.Signing.Merges = []string{"commitssigned"} - t.Run("CommitsSignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.True(t, branch.Commit.Verification.Verified) - })) - }) - }) -} - -func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { - return doAPICreateFile(ctx, path, &api.CreateFileOptions{ - FileOptions: api.FileOptions{ - BranchName: from, - NewBranchName: to, - Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), - Author: api.Identity{ - Name: user.FullName, - Email: user.Email, - }, - Committer: api.Identity{ - Name: user.FullName, - Email: user.Email, - }, - }, - ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), - }, callback...) -} - -func importTestingKey() (*openpgp.Entity, error) { - if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { - return nil, err - } - keyringFile, err := os.Open("tests/integration/private-testing.key") - if err != nil { - return nil, err - } - defer keyringFile.Close() - - block, err := armor.Decode(keyringFile) - if err != nil { - return nil, err - } - - keyring, err := openpgp.ReadKeyRing(block.Body) - if err != nil { - return nil, fmt.Errorf("Keyring access failed: '%w'", err) - } - - // There should only be one entity in this file. - return keyring[0], nil -} diff --git a/tests/integration/repo_sync_fork_test.go b/tests/integration/repo_sync_fork_test.go new file mode 100644 index 0000000000..956494cfc6 --- /dev/null +++ b/tests/integration/repo_sync_fork_test.go @@ -0,0 +1,117 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "forgejo.org/models/auth" + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + api "forgejo.org/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func syncForkTest(t *testing.T, forkName, urlPart string, webSync bool) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) + + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID}) + + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + /// Create a new fork + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.LowerName), &api.CreateForkOption{Name: &forkName}).AddTokenAuth(token) + MakeRequest(t, req, http.StatusAccepted) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var syncForkInfo *api.SyncForkInfo + DecodeJSON(t, resp, &syncForkInfo) + + // This is a new fork, so the commits in both branches should be the same + assert.False(t, syncForkInfo.Allowed) + assert.Equal(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit) + + // Make a commit on the base branch + err := createOrReplaceFileInBranch(baseUser, baseRepo, "sync_fork.txt", "master", "Hello") + require.NoError(t, err) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &syncForkInfo) + + // The commits should no longer be the same and we can sync + assert.True(t, syncForkInfo.Allowed) + assert.NotEqual(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit) + + // Sync the fork + if webSync { + session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s/sync_fork/master", user.Name, forkName), http.StatusSeeOther) + } else { + req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + } + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/%s", user.Name, forkName, urlPart).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &syncForkInfo) + + // After the sync both commits should be the same again + assert.False(t, syncForkInfo.Allowed) + assert.Equal(t, syncForkInfo.BaseCommit, syncForkInfo.ForkCommit) +} + +func TestAPIRepoSyncForkDefault(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + syncForkTest(t, "SyncForkDefault", "sync_fork", false) + }) +} + +func TestAPIRepoSyncForkBranch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + syncForkTest(t, "SyncForkBranch", "sync_fork/master", false) + }) +} + +func TestWebRepoSyncForkBranch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + syncForkTest(t, "SyncForkBranch", "sync_fork/master", true) + }) +} + +func TestWebRepoSyncForkHomepage(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + forkName := "SyncForkHomepage" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20}) + + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID}) + + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + /// Create a new fork + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.LowerName), &api.CreateForkOption{Name: &forkName}).AddTokenAuth(token) + MakeRequest(t, req, http.StatusAccepted) + + // Make a commit on the base branch + err := createOrReplaceFileInBranch(baseUser, baseRepo, "sync_fork.txt", "master", "Hello") + require.NoError(t, err) + + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s", user.Name, forkName), http.StatusOK) + + assert.Contains(t, resp.Body.String(), fmt.Sprintf("This branch is 1 commit behind user2/repo1:master", u.Port())) + }) +} diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 149b8b8d70..bac311f64e 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesDelete() *api.FileResponse { } } -func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse { +func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string, lastCommitWhen time.Time) *api.FileResponse { treePath := "new/file.txt" encoding := "base64" content := "VGhpcyBpcyBhIE5FVyBmaWxl" @@ -118,18 +118,19 @@ func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) * downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath return &api.FileResponse{ Content: &api.ContentsResponse{ - Name: filepath.Base(treePath), - Path: treePath, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", - LastCommitSHA: lastCommitSHA, - Type: "file", - Size: 18, - Encoding: &encoding, - Content: &content, - URL: &selfURL, - HTMLURL: &htmlURL, - GitURL: &gitURL, - DownloadURL: &downloadURL, + Name: filepath.Base(treePath), + Path: treePath, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", + LastCommitSHA: lastCommitSHA, + LastCommitWhen: lastCommitWhen, + Type: "file", + Size: 18, + Encoding: &encoding, + Content: &content, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, Links: &api.FileLinksResponse{ Self: &selfURL, GitURL: &gitURL, @@ -177,7 +178,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) * } } -func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA string) *api.FileResponse { +func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA string, lastCommitWhen time.Time) *api.FileResponse { encoding := "base64" content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ==" selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master" @@ -186,18 +187,19 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename return &api.FileResponse{ Content: &api.ContentsResponse{ - Name: filename, - Path: filename, - SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", - LastCommitSHA: lastCommitSHA, - Type: "file", - Size: 43, - Encoding: &encoding, - Content: &content, - URL: &selfURL, - HTMLURL: &htmlURL, - GitURL: &gitURL, - DownloadURL: &downloadURL, + Name: filename, + Path: filename, + SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", + LastCommitSHA: lastCommitSHA, + LastCommitWhen: lastCommitWhen, + Type: "file", + Size: 43, + Encoding: &encoding, + Content: &content, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, Links: &api.FileLinksResponse{ Self: &selfURL, GitURL: &gitURL, @@ -264,7 +266,7 @@ func TestChangeRepoFiles(t *testing.T) { require.NoError(t, err) lastCommit, err := gitRepo.GetCommitByPath("new/file.txt") require.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String()) + expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String(), lastCommit.Committer.When) assert.Equal(t, expectedFileResponse.Content, filesResponse.Files[0]) assert.Equal(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) assert.Equal(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) @@ -282,7 +284,7 @@ func TestChangeRepoFiles(t *testing.T) { require.NoError(t, err) lastCommit, err := commit.GetCommitByPath(opts.Files[0].TreePath) require.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String(), lastCommit.Committer.When) assert.Equal(t, expectedFileResponse.Content, filesResponse.Files[0]) assert.Equal(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) assert.Equal(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) @@ -303,7 +305,7 @@ func TestChangeRepoFiles(t *testing.T) { require.NoError(t, err) lastCommit, err := commit.GetCommitByPath(opts.Files[0].TreePath) require.NoError(t, err) - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String(), lastCommit.Committer.When) // assert that the old file no longer exists in the last commit of the branch fromEntry, err := commit.GetTreeEntryByPath(opts.Files[0].FromTreePath) @@ -339,7 +341,7 @@ func TestChangeRepoFiles(t *testing.T) { commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch) lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String(), lastCommit.Committer.When) assert.Equal(t, expectedFileResponse.Content, filesResponse.Files[0]) }) diff --git a/tests/integration/signing_git_test.go b/tests/integration/signing_git_test.go new file mode 100644 index 0000000000..bc662ee3bb --- /dev/null +++ b/tests/integration/signing_git_test.go @@ -0,0 +1,330 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "encoding/base64" + "fmt" + "net/url" + "os" + "path/filepath" + "testing" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/git" + "forgejo.org/modules/process" + "forgejo.org/modules/setting" + api "forgejo.org/modules/structs" + "forgejo.org/modules/test" + "forgejo.org/tests" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestInstanceSigning(t *testing.T) { + t.Cleanup(func() { + // Cannot use t.Context(), it is in the done state. + require.NoError(t, git.InitFull(context.Background())) //nolint:usetesting + }) + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() + defer test.MockProtect(&setting.Repository.Signing.InitialCommit)() + defer test.MockProtect(&setting.Repository.Signing.CRUDActions)() + + t.Run("SSH", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pubKeyContent, err := os.ReadFile("tests/integration/ssh-signing-key.pub") + require.NoError(t, err) + + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent) + require.NoError(t, err) + signingKeyPath, err := filepath.Abs("tests/integration/ssh-signing-key") + require.NoError(t, err) + require.NoError(t, os.Chmod(signingKeyPath, 0o600)) + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() + defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, signingKeyPath)() + + // Ensure the git config is updated with the new signing format. + require.NoError(t, git.InitFull(t.Context())) + + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { + u2 := *u + testCRUD(t, &u2, "ssh", objectFormat) + }) + }) + + t.Run("PGP", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Use a new GNUPGPHOME to avoid messing with the existing GPG keyring. + tmpDir := t.TempDir() + require.NoError(t, os.Chmod(tmpDir, 0o700)) + t.Setenv("GNUPGHOME", tmpDir) + + rootKeyPair, err := importTestingKey() + require.NoError(t, err) + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() + defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")() + + // Ensure the git config is updated with the new signing format. + require.NoError(t, git.InitFull(t.Context())) + + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { + u2 := *u + testCRUD(t, &u2, "pgp", objectFormat) + }) + }) + }) +} + +func testCRUD(t *testing.T, u *url.URL, signingFormat string, objectFormat git.ObjectFormat) { + t.Helper() + setting.Repository.Signing.CRUDActions = []string{"never"} + setting.Repository.Signing.InitialCommit = []string{"never"} + + username := "user2" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) + baseAPITestContext := NewAPITestContext(t, username, "repo1") + u.Path = baseAPITestContext.GitPath() + + suffix := "-" + signingFormat + "-" + objectFormat.Name() + + t.Run("Unsigned-Initial", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.InitialCommit = []string{"never"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( + t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.InitialCommit = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + assert.Equal(t, "fox@example.com", branch.Commit.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"never"} + + testCtx := NewAPITestContext(t, username, "initial-always-never"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-always-parent"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-always-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("UnsignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"commitssigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + }) + + t.Run("BaseSignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"basesigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + }) + + t.Run("CommitsSignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"commitssigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + })) + }) +} + +func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { + return doAPICreateFile(ctx, path, &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: from, + NewBranchName: to, + Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), + Author: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + Committer: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), + }, callback...) +} + +func importTestingKey() (*openpgp.Entity, error) { + if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { + return nil, err + } + keyringFile, err := os.Open("tests/integration/private-testing.key") + if err != nil { + return nil, err + } + defer keyringFile.Close() + + block, err := armor.Decode(keyringFile) + if err != nil { + return nil, err + } + + keyring, err := openpgp.ReadKeyRing(block.Body) + if err != nil { + return nil, fmt.Errorf("Keyring access failed: '%w'", err) + } + + // There should only be one entity in this file. + return keyring[0], nil +} diff --git a/tests/integration/ssh-signing-key b/tests/integration/ssh-signing-key new file mode 100644 index 0000000000..e67df477c5 --- /dev/null +++ b/tests/integration/ssh-signing-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhwAAAJhlmhmkZZoZ +pAAAAAtzc2gtZWQyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhw +AAAEDnOTuE2rDECN+2OsuUbQgGrMSY22tn+IF5JG5nuyJinVeRC8GfFyXtiy0f1E7hLv77 +BXW7e68tFvIcs8/29YqHAAAAE2d1c3RlZEBndXN0ZWQtYmVhc3QBAg== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/integration/ssh-signing-key.pub b/tests/integration/ssh-signing-key.pub new file mode 100644 index 0000000000..3d1ab60b47 --- /dev/null +++ b/tests/integration/ssh-signing-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH diff --git a/tests/integration/size_translations_test.go b/tests/integration/translations_test.go similarity index 77% rename from tests/integration/size_translations_test.go rename to tests/integration/translations_test.go index d34cd0b490..9cfa3423b7 100644 --- a/tests/integration/size_translations_test.go +++ b/tests/integration/translations_test.go @@ -1,6 +1,5 @@ // Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - +// SPDX-License-Identifier: GPL-3.0-or-later package integration import ( @@ -13,6 +12,7 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/translation/i18n" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" @@ -20,6 +20,29 @@ import ( "github.com/stretchr/testify/assert" ) +func TestMissingTranslationHandling(t *testing.T) { + // Currently new languages can only be added to localestore via AddLocaleByIni + // so this line is here to make the other one work. When INI locales are removed, + // it will not be needed by this test. + i18n.DefaultLocales.AddLocaleByIni("fun", "Funlang", nil, []byte(""), nil) + + // Add a testing locale to the store + i18n.DefaultLocales.AddToLocaleFromJSON("fun", []byte(`{ + "meta.last_line": "This language only has one line that is never used by the UI. It will never have a translation for incorrect_root_url" + }`)) + + // Get "fun" locale, make sure it's available + funLocale, found := i18n.DefaultLocales.Locale("fun") + assert.True(t, found) + + // Get translation for a string that this locale doesn't have + s := funLocale.TrString("incorrect_root_url") + + // Verify fallback to English + assert.True(t, strings.HasPrefix(s, "This Forgejo instance")) +} + +// TestDataSizeTranslation is a test for usage of TrSize in file size display func TestDataSizeTranslation(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { testUser := "user2" @@ -103,14 +126,14 @@ func testFileSizeTranslated(t *testing.T, session *TestSession, filePath, correc resp := session.MakeRequest(t, req, http.StatusOK) // Check if file size is translated - sizeCorrent := false + sizeCorrect := false fileInfo := NewHTMLParser(t, resp.Body).Find(".file-info .file-info-entry") fileInfo.Each(func(i int, info *goquery.Selection) { infoText := strings.TrimSpace(info.Text()) if infoText == correctSize { - sizeCorrent = true + sizeCorrect = true } }) - assert.True(t, sizeCorrent) + assert.True(t, sizeCorrect) } diff --git a/tools/generate-images.js b/tools/generate-images.js index d28e0916f7..06f05892fb 100755 --- a/tools/generate-images.js +++ b/tools/generate-images.js @@ -1,9 +1,8 @@ -#!/usr/bin/env node -import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line import-x/no-unresolved -import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line import-x/no-unresolved import {optimize} from 'svgo'; import {readFile, writeFile} from 'node:fs/promises'; -import {argv, exit} from 'node:process'; +import {exit} from 'node:process'; +import SharpConstructor from 'sharp'; +import {fileURLToPath} from 'node:url'; function doExit(err) { if (err) console.error(err); @@ -28,36 +27,14 @@ async function generate(svg, path, {size, bg}) { return; } - const {objects, options} = await loadSVGFromString(svg); - const canvas = new Canvas(); - canvas.setDimensions({width: size, height: size}); - const ctx = canvas.getContext('2d'); - ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); - + let sharp = (new SharpConstructor(Buffer.from(svg))).resize(size, size).png({compressionLevel: 9, palette: true, effort: 10, quality: 80}); if (bg) { - canvas.add(new Rect({ - left: 0, - top: 0, - height: size * (1 / (size / options.height)), - width: size * (1 / (size / options.width)), - fill: 'white', - })); + sharp = sharp.flatten({background: 'white'}); } - - canvas.add(util.groupSVGElements(objects, options)); - canvas.renderAll(); - - let png = Buffer.from([]); - for await (const chunk of canvas.createPNGStream()) { - png = Buffer.concat([png, chunk]); - } - - png = await imageminZopfli({more: true})(png); - await writeFile(outputFile, png); + sharp.toFile(fileURLToPath(outputFile), (err) => err !== null && console.error(err) && exit(1)); } async function main() { - const gitea = argv.slice(2).includes('gitea'); const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8'); const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8'); @@ -68,7 +45,6 @@ async function main() { generate(faviconSvg, '../public/assets/img/favicon.png', {size: 180}), generate(logoSvg, '../public/assets/img/avatar_default.png', {size: 200}), generate(logoSvg, '../public/assets/img/apple-touch-icon.png', {size: 180, bg: true}), - gitea && generate(logoSvg, '../public/assets/img/gitea.svg', {size: 32}), ]); } diff --git a/web_src/css/base.css b/web_src/css/base.css index bfe8cd54ae..559a035c2c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -18,6 +18,7 @@ /* other variables */ --border-radius: 4px; --border-radius-medium: 6px; + --border-radius-large: 25px; --border-radius-full: 99999px; /* TODO: use calc(infinity * 1px) */ --opacity-disabled: 0.55; --height-loading: 16rem; @@ -1053,10 +1054,11 @@ overflow-menu .overflow-menu-items { overflow-menu .overflow-menu-items .item { margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */ + height: 100%; } overflow-menu .overflow-menu-items .item > .svg { - align-self: baseline; + align-self: center; } overflow-menu .ui.label { @@ -1282,10 +1284,6 @@ svg.text.purple, width: 100%; } -.migrate .svg.gitea-git { - color: var(--color-git); -} - .color-icon { display: inline-block; border-radius: var(--border-radius-full); @@ -1363,10 +1361,6 @@ table th[data-sortt-desc] .svg { border-color: var(--color-secondary); } -.ui.tabular.menu .item { - height: 100%; -} - .ui.tabular.menu .item, .ui.secondary.pointing.menu .item { padding: 11px 12px !important; diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css index f190c7eb1f..b151080c64 100644 --- a/web_src/css/editor/combomarkdowneditor.css +++ b/web_src/css/editor/combomarkdowneditor.css @@ -11,6 +11,18 @@ flex-wrap: wrap; } +.markdown-toolbar-switch { + display: flex; + height: 30px; +} +.markdown-toolbar-switch .switch .item { + padding: 0.25em 1em; +} + +.markdown-toolbar-hidden .markdown-toolbar-button { + display: none; +} + .combo-markdown-editor .markdown-toolbar-group { display: flex; } diff --git a/web_src/css/index.css b/web_src/css/index.css index eec17eab13..0a5a0180aa 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -75,6 +75,7 @@ @import "./explore.css"; @import "./review.css"; @import "./actions.css"; +@import "./migrate.css"; @tailwind utilities; @import "./helpers.css"; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 947480a7e8..6574d413fc 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -2,7 +2,7 @@ overflow: hidden; font-size: 16px; line-height: 1.5 !important; - word-wrap: break-word; + overflow-wrap: anywhere; } .markup > *:first-child { diff --git a/web_src/css/migrate.css b/web_src/css/migrate.css new file mode 100644 index 0000000000..d5e909b42d --- /dev/null +++ b/web_src/css/migrate.css @@ -0,0 +1,68 @@ +.migrate .svg.gitea-git { + --git-logo-color: #f05133; + color: var(--git-logo-color); +} + +.migrate-entries { + display: grid; + /* Limited to 4 cols by 1280px container */ + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); + gap: 1.5rem; +} + +.migrate-entry svg { + padding: 1.5rem; +} + +.migrate-entry { + display: flex; + flex-direction: column; + color: var(--color-text); + background: var(--color-card); + border: 1px solid var(--fancy-card-border); + border-radius: var(--border-radius-large); + transition: all 0.1s ease-in-out; +} + +.migrate-entry:hover { + transform: scale(105%); + box-shadow: 0 0.5rem 1rem var(--color-shadow); + color: var(--color-text); +} + +.migrate-entry .content { + width: 100%; + margin-top: .5rem; + padding: 1rem; + flex: 1; +} + +.migrate-entry .description { + margin-top: .5rem; + text-wrap: balance; +} + +/* Desktop layout features */ +@media (min-width: 599.98px) { + .migrate-entry .content { + text-align: center; + border-top: 1px solid var(--fancy-card-border); + border-radius: 0 0 var(--border-radius-large) var(--border-radius-large); + background: var(--fancy-card-bg); + } +} + +/* Mobile layout features */ +@media (max-width: 600px) { + .migrate-entries { + grid-template-columns: repeat(1, 1fr); + } + .migrate-entry { + flex-direction: row; + } + .migrate-entry svg { + height: 100%; + width: 100%; + max-width: 128px; + } +} diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css index 1d0bf83390..7ef4618a18 100644 --- a/web_src/css/modules/dimmer.css +++ b/web_src/css/modules/dimmer.css @@ -1,3 +1,7 @@ +body:has(> .ui.active.dimmer) { + overflow: hidden; +} + .ui.active.dimmer { display: flex; opacity: 1; @@ -10,8 +14,9 @@ display: none; flex-direction: column; height: 100%; - position: absolute; + position: fixed; opacity: 0; + transform-origin: center center; justify-content: center; user-select: none; width: 100%; diff --git a/web_src/css/modules/switch.css b/web_src/css/modules/switch.css index a9499a84aa..7780155787 100644 --- a/web_src/css/modules/switch.css +++ b/web_src/css/modules/switch.css @@ -11,6 +11,7 @@ .switch .item { display: flex; + gap: 0.5rem; align-items: center; padding: .5em 1.125em; color: var(--color-text); diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f498a992ed..80fd2be00d 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2764,37 +2764,6 @@ tbody.commit-list { border-left: 1px solid var(--color-secondary); } -.migrate-entries { - display: grid !important; - grid-template-columns: repeat(3, 1fr); - gap: 25px; - margin: 0 !important; -} - -@media (max-width: 767.98px) { - .migrate-entries { - grid-template-columns: repeat(1, 1fr); - } -} - -.migrate-entry { - transition: all 0.1s ease-in-out; - box-shadow: none !important; - border: 1px solid var(--color-secondary); - color: var(--color-text) !important; - width: auto !important; - margin: 0 !important; -} - -.migrate-entry:hover { - transform: scale(105%); - box-shadow: 0 0.5rem 1rem var(--color-shadow) !important; -} - -.migrate-entry .description { - text-wrap: balance; -} - .commits-table .commits-table-right form { display: flex; align-items: center; diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 07ae5a5683..7f1b8c31cd 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -69,17 +69,17 @@ } } -#issue-list .flex-item-body .branches { - display: inline-flex; +#issue-list .issue-meta { + gap: 0 0.5rem; } -#issue-list .flex-item-body .branches .branch { +#issue-list .issue-meta .branch { background-color: var(--color-secondary-alpha-50); border-radius: var(--border-radius); padding: 0 4px; } -#issue-list .flex-item-body .branches .truncated-name { +#issue-list .issue-meta .branch .truncated-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -88,18 +88,18 @@ vertical-align: top; } -#issue-list .flex-item-body .checklist progress { +#issue-list .issue-meta .checklist progress { margin-left: 2px; width: 80px; height: 6px; display: inline-block; } -#issue-list .flex-item-body .checklist progress::-webkit-progress-value { +#issue-list .issue-meta .checklist progress::-webkit-progress-value { background-color: var(--color-secondary-dark-4); } -#issue-list .flex-item-body .checklist progress::-moz-progress-bar { +#issue-list .issue-meta .checklist progress::-moz-progress-bar { background-color: var(--color-secondary-dark-4); } diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css index c9c538502d..b1b80510d4 100644 --- a/web_src/css/themes/theme-forgejo-dark.css +++ b/web_src/css/themes/theme-forgejo-dark.css @@ -196,7 +196,6 @@ --color-orange-badge: #ea580c; --color-orange-badge-bg: #ea580c22; --color-orange-badge-hover-bg: #ea580c44; - --color-git: #f05133; /* Icon colors (PR/Issue/...) */ --color-icon-green: #3fb950; --color-icon-red: #f85149; @@ -228,6 +227,8 @@ --color-active: var(--steel-650); --color-menu: var(--steel-700); --color-card: var(--steel-700); + --fancy-card-bg: var(--steel-650); + --fancy-card-border: var(--steel-600); --color-markup-table-row: #ffffff06; --color-markup-code-block: var(--steel-800); --color-markup-code-inline: var(--steel-850); diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css index a5e4ffe050..277b52165f 100644 --- a/web_src/css/themes/theme-forgejo-light.css +++ b/web_src/css/themes/theme-forgejo-light.css @@ -212,7 +212,6 @@ --color-orange-badge: #ea580c; --color-orange-badge-bg: #ea580c22; --color-orange-badge-hover-bg: #ea580c44; - --color-git: #f05133; /* Icon colors (PR/Issue/...) */ --color-icon-green: var(--color-green-light); --color-icon-red: var(--color-red-light); @@ -244,6 +243,8 @@ --color-active: #d4d4d8aa; --color-menu: var(--zinc-100); --color-card: var(--zinc-50); + --fancy-card-bg: var(--zinc-100); + --fancy-card-border: var(--zinc-200); --color-markup-table-row: #ffffff06; --color-markup-code-block: var(--zinc-150); --color-markup-code-inline: var(--zinc-200); diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 6ad6efe748..7ba428f0b7 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -179,7 +179,6 @@ --color-orange-badge: #f2711c; --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; - --color-git: #f05133; /* Icon colors (PR/Issue/...) */ --color-icon-green: var(--color-green); --color-icon-red: var(--color-red); @@ -209,6 +208,8 @@ --color-active: #e8e8ff24; --color-menu: #151a1e; --color-card: #151a1e; + --fancy-card-bg: #14171a; + --fancy-card-border: #3b444a; --color-markup-table-row: #e8e8ff0f; --color-markup-code-block: #e8e8ff12; --color-markup-code-inline: #e8e8ff28; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 830b96febe..8ad89f44a4 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -179,7 +179,6 @@ --color-orange-badge: #f2711c; --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; - --color-git: #f05133; /* Icon colors (PR/Issue/...) */ --color-icon-green: var(--color-green); --color-icon-red: var(--color-red); @@ -209,6 +208,8 @@ --color-active: #00001714; --color-menu: #f8f9fb; --color-card: #f8f9fb; + --fancy-card-bg: #ffffff; + --fancy-card-border: #d0d7de; --color-markup-table-row: #0030600a; --color-markup-code-block: #00306010; --color-markup-code-inline: #00306012; diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json index 9d5ec0ca2a..72b04caf36 100644 --- a/web_src/fomantic/package-lock.json +++ b/web_src/fomantic/package-lock.json @@ -132,17 +132,17 @@ } }, "node_modules/@octokit/core": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz", - "integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", + "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", "license": "MIT", "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.1.2", - "@octokit/request": "^9.2.1", - "@octokit/request-error": "^6.1.7", - "@octokit/types": "^13.6.2", + "@octokit/graphql": "^8.2.2", + "@octokit/request": "^9.2.3", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" }, @@ -161,13 +161,13 @@ } }, "node_modules/@octokit/core/node_modules/@octokit/endpoint": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", - "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^13.6.2", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" }, "engines": { @@ -175,22 +175,22 @@ } }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", "license": "MIT", "peer": true }, "node_modules/@octokit/core/node_modules/@octokit/request": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz", - "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^10.1.3", - "@octokit/request-error": "^6.1.7", - "@octokit/types": "^13.6.2", + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" }, @@ -199,26 +199,26 @@ } }, "node_modules/@octokit/core/node_modules/@octokit/request-error": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", - "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^13.6.2" + "@octokit/types": "^14.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/core/node_modules/before-after-hook": { @@ -253,14 +253,14 @@ "license": "ISC" }, "node_modules/@octokit/graphql": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz", - "integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/request": "^9.2.2", - "@octokit/types": "^13.8.0", + "@octokit/request": "^9.2.3", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { @@ -268,13 +268,13 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz", - "integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^13.6.2", + "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" }, "engines": { @@ -282,22 +282,22 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", "license": "MIT", "peer": true }, "node_modules/@octokit/graphql/node_modules/@octokit/request": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz", - "integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", + "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^10.1.3", - "@octokit/request-error": "^6.1.7", - "@octokit/types": "^13.6.2", + "@octokit/endpoint": "^10.1.4", + "@octokit/request-error": "^6.1.8", + "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" }, @@ -306,26 +306,26 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz", - "integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^13.6.2" + "@octokit/types": "^14.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@octokit/openapi-types": "^25.0.0" } }, "node_modules/@octokit/graphql/node_modules/universal-user-agent": { @@ -494,12 +494,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/vinyl": { @@ -1249,9 +1249,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001713", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", + "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", "funding": [ { "type": "opencollective", @@ -2005,9 +2005,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.128", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", - "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "version": "1.5.136", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz", + "integrity": "sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -8226,9 +8226,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/union-value": { diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 12ff564aa7..81b1244328 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -54,12 +54,12 @@ const sfc = { if (this.viewType === 'tree') { this.isViewTree = true; this.refNameText = this.commitIdShort; - } else if (this.viewType === 'tag') { - this.isViewTag = true; - this.refNameText = this.tagName; - } else { + } else if (this.viewType === 'branch') { this.isViewBranch = true; this.refNameText = this.branchName; + } else { + this.isViewTag = true; + this.refNameText = this.tagName; } document.body.addEventListener('click', (event) => { @@ -252,7 +252,8 @@ export default sfc; // activate IDE's Vue plugin diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index b46f4f8a74..76cf4e57f7 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -199,7 +199,7 @@ export function initAdminCommon() { } } - if (document.querySelector('.admin.authentication')) { + if (document.querySelector('.admin.edit.authentication, .admin.new.authentication')) { const authNameEl = document.getElementById('auth_name'); authNameEl.addEventListener('input', (el) => { // appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash. diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index efd068f354..53c6b85728 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -151,7 +151,7 @@ class ComboMarkdownEditor { setupTab() { const $container = $(this.container); - const tabs = $container[0].querySelectorAll('.tabular.menu > .item'); + const tabs = $container[0].querySelectorAll('.switch > .item'); // Fomantic Tab requires the "data-tab" to be globally unique. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. @@ -159,12 +159,14 @@ class ComboMarkdownEditor { const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer'); tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`); tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`); + const toolbar = $container[0].querySelector('markdown-toolbar'); const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]'); const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]'); panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`); panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`); tabEditor.addEventListener('click', () => { + toolbar.classList.remove('markdown-toolbar-hidden'); requestAnimationFrame(() => { this.focus(); }); @@ -177,6 +179,7 @@ class ComboMarkdownEditor { this.previewMode = this.options.previewMode ?? 'comment'; this.previewWiki = this.options.previewWiki ?? false; tabPreviewer.addEventListener('click', async () => { + toolbar.classList.add('markdown-toolbar-hidden'); const formData = new FormData(); formData.append('mode', this.previewMode); formData.append('context', this.previewContext); diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js index 038336fc0d..f4fe2f40fd 100644 --- a/web_src/js/features/repo-common.js +++ b/web_src/js/features/repo-common.js @@ -58,7 +58,7 @@ export function initRepoCloneLink() { export function initRepoCommonBranchOrTagDropdown(selector) { $(selector).each(function () { const $dropdown = $(this); - $dropdown.find('.reference.column').on('click', function () { + $dropdown.find('.branch-tag-item').on('click', function () { hideElem($dropdown.find('.scrolling.reference-list-menu')); showElem($($(this).data('target'))); return false; diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index c74ba1efbe..4c2ff50574 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -72,7 +72,7 @@ export function initRepoCommentForm() { $selectBranch.find('.ui .branch-name').text(selectedValue); } }); - $selectBranch.find('.reference.column').on('click', function () { + $selectBranch.find('.branch-tag-item').on('click', function () { hideElem($selectBranch.find('.scrolling.reference-list-menu')); $selectBranch.find('.reference .text').removeClass('black'); showElem($($(this).data('target'))); @@ -469,7 +469,7 @@ async function onEditContent(event) { editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset); editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh); } else { - const tabEditor = editContentZone.querySelector('.combo-markdown-editor').querySelector('.tabular.menu > a[data-tab-for=markdown-writer]'); + const tabEditor = editContentZone.querySelector('.combo-markdown-editor').querySelector('.switch > a[data-tab-for=markdown-writer]'); tabEditor?.click(); } diff --git a/web_src/svg/gitea-forgejo.svg b/web_src/svg/gitea-forgejo.svg index e00e5963cf..9daa1ff8f3 100644 --- a/web_src/svg/gitea-forgejo.svg +++ b/web_src/svg/gitea-forgejo.svg @@ -1,5 +1,5 @@ -