Skip to content

Commit f013a64

Browse files
committed
Merge remote-tracking branch 'origin/main' into diffhead
* origin/main: Closed milestones with no issues now show as 100% completed (go-gitea#36220) Show edit page confirmation dialog on tree view file change (go-gitea#36130) Fix regression in writing authorized principals (go-gitea#36213) [skip ci] Updated translations via Crowdin Convert locale files from ini to json format (go-gitea#35489)
2 parents ab3dea7 + 495fee4 commit f013a64

File tree

116 files changed

+201164
-79222
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+201164
-79222
lines changed

.github/workflows/files-changed.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- "Makefile"
5454
- ".golangci.yml"
5555
- ".editorconfig"
56-
- "options/locale/locale_en-US.ini"
56+
- "options/locale/locale_en-US.json"
5757
5858
frontend:
5959
- "*.js"

Makefile

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ WEB_DIRS := web_src/js web_src/css
166166

167167
ESLINT_FILES := web_src/js tools *.ts tests/e2e
168168
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
169-
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
170-
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
169+
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
170+
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
171171

172172
GO_SOURCES := $(wildcard *.go)
173173
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
@@ -911,16 +911,6 @@ lockfile-check:
911911
exit 1; \
912912
fi
913913

914-
.PHONY: update-translations
915-
update-translations:
916-
mkdir -p ./translations
917-
cd ./translations && curl -L https://crowdin.com/download/project/gitea.zip > gitea.zip && unzip gitea.zip
918-
rm ./translations/gitea.zip
919-
$(SED_INPLACE) -e 's/="/=/g' -e 's/"$$//g' ./translations/*.ini
920-
$(SED_INPLACE) -e 's/\\"/"/g' ./translations/*.ini
921-
mv ./translations/*.ini ./options/locale/
922-
rmdir ./translations
923-
924914
.PHONY: generate-gitignore
925915
generate-gitignore: ## update gitignore files
926916
$(GO) run build/generate-gitignores.go

build/update-locales.sh

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,22 @@
11
#!/bin/sh
22

33
# this script runs in alpine image which only has `sh` shell
4-
5-
set +e
6-
if sed --version 2>/dev/null | grep -q GNU; then
7-
SED_INPLACE="sed -i"
8-
else
9-
SED_INPLACE="sed -i ''"
10-
fi
11-
set -e
12-
13-
if [ ! -f ./options/locale/locale_en-US.ini ]; then
4+
if [ ! -f ./options/locale/locale_en-US.json ]; then
145
echo "please run this script in the root directory of the project"
156
exit 1
167
fi
178

18-
mv ./options/locale/locale_en-US.ini ./options/
19-
20-
# the "ini" library for locale has many quirks, its behavior is different from Crowdin.
21-
# see i18n_test.go for more details
22-
23-
# this script helps to unquote the Crowdin outputs for the quirky ini library
24-
# * find all `key="...\"..."` lines
25-
# * remove the leading quote
26-
# * remove the trailing quote
27-
# * unescape the quotes
28-
# * eg: key="...\"..." => key=..."...
29-
$SED_INPLACE -r -e '/^[-.A-Za-z0-9_]+[ ]*=[ ]*".*"$/ {
30-
s/^([-.A-Za-z0-9_]+)[ ]*=[ ]*"/\1=/
31-
s/"$//
32-
s/\\"/"/g
33-
}' ./options/locale/*.ini
34-
35-
# * if the escaped line is incomplete like `key="...` or `key=..."`, quote it with backticks
36-
# * eg: key="... => key=`"...`
37-
# * eg: key=..." => key=`..."`
38-
$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*(".*[^"])$/\1=`\2`/' ./options/locale/*.ini
39-
$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*([^"].*")$/\1=`\2`/' ./options/locale/*.ini
9+
mv ./options/locale/locale_en-US.json ./options/
4010

4111
# Remove translation under 25% of en_us
42-
baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1)
12+
baselines=$(wc -l "./options/locale_en-US.json" | cut -d" " -f1)
4313
baselines=$((baselines / 4))
44-
for filename in ./options/locale/*.ini; do
14+
for filename in ./options/locale/*.json; do
4515
lines=$(wc -l "$filename" | cut -d" " -f1)
4616
if [ $lines -lt $baselines ]; then
4717
echo "Removing $filename: $lines/$baselines"
4818
rm "$filename"
4919
fi
5020
done
5121

52-
mv ./options/locale_en-US.ini ./options/locale/
22+
mv ./options/locale_en-US.json ./options/locale/

crowdin.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ base_path: "."
44
base_url: "https://api.crowdin.com"
55
preserve_hierarchy: true
66
files:
7-
- source: "/options/locale/locale_en-US.ini"
8-
translation: "/options/locale/locale_%locale%.ini"
9-
type: "ini"
7+
- source: "/options/locale/locale_en-US.json"
8+
translation: "/options/locale/locale_%locale%.json"
9+
type: "json"
1010
skip_untranslated_strings: true
1111
export_only_approved: true
1212
update_option: "update_as_unapproved"

models/asymkey/ssh_key_authorized_keys.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"os"
1212
"path/filepath"
13+
"regexp"
1314
"strings"
1415
"sync"
1516

@@ -50,12 +51,42 @@ func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
5051
return err
5152
}
5253

54+
var globalVars = sync.OnceValue(func() (ret struct {
55+
principalRegexp *regexp.Regexp
56+
},
57+
) {
58+
// principalRegexp expresses whether a principal is considered valid.
59+
// This reverse engineers how sshd parses the authorized keys file,
60+
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
61+
// Any newline or # comment will be stripped when parsing, so don't allow
62+
// those. Also, if any space or tab is present in the principal, the part
63+
// proceeding this would be parsed as an option, so just avoid any whitespace
64+
// altogether.
65+
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
66+
return ret
67+
})
68+
5369
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
54-
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
55-
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
56-
if err != nil {
57-
return false, err
70+
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
71+
72+
var sshKey string
73+
74+
if key.Type == KeyTypePrincipal {
75+
// TODO: actually using PublicKey to store "principal" is an abuse
76+
if !globalVars().principalRegexp.MatchString(key.Content) {
77+
return false, fmt.Errorf("invalid principal key: %s", key.Content)
78+
}
79+
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
80+
} else {
81+
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
82+
if err != nil {
83+
return false, err
84+
}
85+
86+
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
87+
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
5888
}
89+
5990
// now the key is valid, the code below could only return template/IO related errors
6091
sbCmd := &strings.Builder{}
6192
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
@@ -69,9 +100,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
69100
return true, err
70101
}
71102
sshCommandEscaped := util.ShellEscape(sbCmd.String())
72-
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
73-
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
74-
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
103+
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
75104
return true, err
76105
}
77106

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package asymkey
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/test"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestWriteAuthorizedStringForKey(t *testing.T) {
17+
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
18+
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
19+
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
20+
sb := &strings.Builder{}
21+
valid, err := writeAuthorizedStringForKey(key, sb)
22+
return valid, sb.String(), err
23+
}
24+
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
25+
26+
testValid := func(t *testing.T, key *PublicKey, expected string) {
27+
valid, content, err := writeKey(t, key)
28+
assert.True(t, valid)
29+
assert.Equal(t, expected, content)
30+
assert.NoError(t, err)
31+
}
32+
33+
testInvalid := func(t *testing.T, key *PublicKey) {
34+
valid, content, err := writeKey(t, key)
35+
assert.False(t, valid)
36+
assert.Empty(t, content)
37+
assert.Error(t, err)
38+
}
39+
40+
t.Run("PublicKey", func(t *testing.T) {
41+
testValid(t, &PublicKey{
42+
OwnerID: 123,
43+
Content: validKeyContent + " any-comment",
44+
Type: KeyTypeUser,
45+
}, `# gitea public key
46+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
47+
`)
48+
})
49+
50+
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
51+
testValid(t, &PublicKey{
52+
OwnerID: 123,
53+
Content: validKeyContent + "\nany-more", // the new line should be ignored
54+
Type: KeyTypeUser,
55+
}, `# gitea public key
56+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
57+
`)
58+
})
59+
60+
t.Run("PublicKeyInvalid", func(t *testing.T) {
61+
testInvalid(t, &PublicKey{
62+
OwnerID: 123,
63+
Content: validKeyContent + "any-more",
64+
Type: KeyTypeUser,
65+
})
66+
})
67+
68+
t.Run("Principal", func(t *testing.T) {
69+
testValid(t, &PublicKey{
70+
OwnerID: 123,
71+
Content: "any-content",
72+
Type: KeyTypePrincipal,
73+
}, `# gitea public key
74+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
75+
`)
76+
})
77+
78+
t.Run("PrincipalInvalid", func(t *testing.T) {
79+
testInvalid(t, &PublicKey{
80+
OwnerID: 123,
81+
Content: "a b",
82+
Type: KeyTypePrincipal,
83+
})
84+
testInvalid(t, &PublicKey{
85+
OwnerID: 123,
86+
Content: "a\nb",
87+
Type: KeyTypePrincipal,
88+
})
89+
})
90+
}

models/issues/milestone.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func init() {
7575
func (m *Milestone) BeforeUpdate() {
7676
if m.NumIssues > 0 {
7777
m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
78+
} else if m.IsClosed {
79+
m.Completeness = 100
7880
} else {
7981
m.Completeness = 0
8082
}
@@ -195,8 +197,8 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
195197
if err != nil {
196198
return err
197199
}
198-
_, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
199-
id,
200+
_, err = e.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END) WHERE id=?",
201+
true, id,
200202
)
201203
return err
202204
}
@@ -240,6 +242,11 @@ func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) err
240242
if count < 1 {
241243
return nil
242244
}
245+
246+
if err := UpdateMilestoneCounters(ctx, m.ID); err != nil {
247+
return err
248+
}
249+
243250
return updateRepoMilestoneNum(ctx, m.RepoID)
244251
}
245252

models/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ func prepareMigrationTasks() []*migration {
398398
// Gitea 1.25.0 ends at migration ID number 322 (database version 323)
399399

400400
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
401+
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
401402
}
402403
return preparedMigrations
403404
}

models/migrations/v1_26/v324.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_26
5+
6+
import (
7+
"fmt"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func FixClosedMilestoneCompleteness(x *xorm.Engine) error {
13+
// Update all milestones to recalculate completeness with the new logic:
14+
// - Closed milestones with 0 issues should show 100%
15+
// - All other milestones should calculate based on closed/total ratio
16+
_, err := x.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END)",
17+
true,
18+
)
19+
if err != nil {
20+
return fmt.Errorf("error updating milestone completeness: %w", err)
21+
}
22+
23+
return nil
24+
}

modules/translation/i18n/i18n.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type LocaleStore interface {
3232
// HasLang returns whether a given language is present in the store
3333
HasLang(langName string) bool
3434
// AddLocaleByIni adds a new language to the store
35-
AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error
35+
AddLocaleByJSON(langName, langDesc string, source, moreSource []byte) error
3636
}
3737

3838
// ResetDefaultLocales resets the current default locales

0 commit comments

Comments
 (0)