diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 081e4a2db4b..88ae98db8df 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -642,7 +642,7 @@ rules: no-this-before-super: [2] no-throw-literal: [2] no-undef-init: [2] - no-undef: [2, {typeof: true}] + no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes no-undefined: [0] no-underscore-dangle: [0] no-unexpected-multiline: [2] diff --git a/flake.nix b/flake.nix index 2c9d74c137e..e3655b627eb 100644 --- a/flake.nix +++ b/flake.nix @@ -29,7 +29,6 @@ poetry # backend - go_1_22 gofumpt sqlite ]; diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index 443effe08c9..d88a8ed8a91 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -1,3 +1,22 @@ +- + id: 46 + attempt: 3 + runner_id: 1 + status: 3 # 3 is the status code for "cancelled" + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa + token_salt: eeeeeeee + token_last_eight: eeeeeeee + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 - id: 47 job_id: 192 diff --git a/models/fixtures/system_setting.yml b/models/fixtures/system_setting.yml index 30542bc82a9..dcad176c899 100644 --- a/models/fixtures/system_setting.yml +++ b/models/fixtures/system_setting.yml @@ -1,7 +1,7 @@ - id: 1 setting_key: 'picture.disable_gravatar' - setting_value: 'false' + setting_value: 'true' version: 1 created: 1653533198 updated: 1653533198 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 1044e487f81..b3bece5589c 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -23,9 +23,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar1 + avatar: "" avatar_email: user1@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -60,8 +60,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar2 + avatar: "" avatar_email: user2@example.com + # cause a random avatar to be generated when referenced for test purposes use_custom_avatar: false num_followers: 2 num_following: 1 @@ -97,9 +98,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar3 + avatar: "" avatar_email: org3@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -134,9 +135,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar4 + avatar: "" avatar_email: user4@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 1 num_stars: 0 @@ -171,9 +172,9 @@ allow_import_local: false allow_create_organization: false prohibit_login: false - avatar: avatar5 + avatar: "" avatar_email: user5@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -208,9 +209,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar6 + avatar: "" avatar_email: org6@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -245,9 +246,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar7 + avatar: "" avatar_email: org7@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -282,9 +283,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar8 + avatar: "" avatar_email: user8@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 1 num_following: 1 num_stars: 0 @@ -319,9 +320,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar9 + avatar: "" avatar_email: user9@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -332,6 +333,7 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: false + created_unix: 1730468968 - id: 10 @@ -356,9 +358,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar10 + avatar: "" avatar_email: user10@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 2 @@ -393,9 +395,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar11 + avatar: "" avatar_email: user11@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -430,9 +432,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar12 + avatar: "" avatar_email: user12@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -467,9 +469,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar13 + avatar: "" avatar_email: user13@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -504,9 +506,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar14 + avatar: "" avatar_email: user13@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -541,9 +543,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar15 + avatar: "" avatar_email: user15@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -578,9 +580,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar16 + avatar: "" avatar_email: user16@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -615,9 +617,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar17 + avatar: "" avatar_email: org17@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -652,9 +654,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar18 + avatar: "" avatar_email: user18@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -689,9 +691,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar19 + avatar: "" avatar_email: org19@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -726,9 +728,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar20 + avatar: "" avatar_email: user20@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -763,9 +765,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar21 + avatar: "" avatar_email: user21@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -800,9 +802,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar22 + avatar: "" avatar_email: limited_org@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -837,9 +839,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar23 + avatar: "" avatar_email: privated_org@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -874,9 +876,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar24 + avatar: "" avatar_email: user24@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -911,9 +913,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar25 + avatar: "" avatar_email: org25@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -948,9 +950,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar26 + avatar: "" avatar_email: org26@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -985,9 +987,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar27 + avatar: "" avatar_email: user27@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1022,9 +1024,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar28 + avatar: "" avatar_email: user28@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1059,9 +1061,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar29 + avatar: "" avatar_email: user29@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1096,9 +1098,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar29 + avatar: "" avatar_email: user30@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1133,9 +1135,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar31 + avatar: "" avatar_email: user31@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 1 num_stars: 0 @@ -1170,9 +1172,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar32 + avatar: "" avatar_email: user30@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1207,9 +1209,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar33 + avatar: "" avatar_email: user33@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 1 num_following: 0 num_stars: 0 @@ -1245,7 +1247,7 @@ allow_import_local: false allow_create_organization: false prohibit_login: false - avatar: avatar34 + avatar: "" avatar_email: user34@example.com use_custom_avatar: true num_followers: 0 @@ -1282,9 +1284,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar35 + avatar: "" avatar_email: private_org35@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1319,9 +1321,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar22 + avatar: "" avatar_email: abcde@gitea.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1356,9 +1358,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: true - avatar: avatar29 + avatar: "" avatar_email: user37@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1393,9 +1395,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar38 + avatar: "" avatar_email: user38@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1430,9 +1432,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar39 + avatar: "" avatar_email: user39@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1467,9 +1469,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar40 + avatar: "" avatar_email: user40@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1504,9 +1506,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar41 + avatar: "" avatar_email: org41@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 @@ -1541,9 +1543,9 @@ allow_import_local: false allow_create_organization: true prohibit_login: false - avatar: avatar42 + avatar: "" avatar_email: org42@example.com - use_custom_avatar: false + use_custom_avatar: true num_followers: 0 num_following: 0 num_stars: 0 diff --git a/models/user/user.go b/models/user/user.go index c1cb988e43d..a2d91662919 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -48,19 +48,19 @@ const ( UserTypeIndividual UserType = iota // Historic reason to make it starts at 0. // UserTypeOrganization defines an organization - UserTypeOrganization + UserTypeOrganization // 1 // UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on - UserTypeUserReserved + UserTypeUserReserved // 2 // UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved - UserTypeOrganizationReserved + UserTypeOrganizationReserved // 3 // UserTypeBot defines a bot user - UserTypeBot + UserTypeBot // 4 // UserTypeRemoteUser defines a remote user for federated users - UserTypeRemoteUser + UserTypeRemoteUser // 5 ) const ( @@ -884,7 +884,13 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error { // GetInactiveUsers gets all inactive users func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) { - var cond builder.Cond = builder.Eq{"is_active": false} + cond := builder.And( + builder.Eq{"is_active": false}, + builder.Or( // only plain user + builder.Eq{"`type`": UserTypeIndividual}, + builder.Eq{"`type`": UserTypeUserReserved}, + ), + ) if olderThan > 0 { cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()}) diff --git a/models/user/user_test.go b/models/user/user_test.go index bc1abc64512..6701be39a55 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -588,3 +588,17 @@ func TestDisabledUserFeatures(t *testing.T) { assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } } + +func TestGetInactiveUsers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // all inactive users + // user1's createdunix is 1730468968 + users, err := user_model.GetInactiveUsers(db.DefaultContext, 0) + assert.NoError(t, err) + assert.Len(t, users, 1) + interval := time.Now().Unix() - 1730468968 + 3600*24 + users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second))) + assert.NoError(t, err) + assert.Len(t, users, 0) +} diff --git a/modules/git/commit.go b/modules/git/commit.go index 86adaa79a66..010b56948ef 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -9,7 +9,6 @@ import ( "bytes" "context" "errors" - "fmt" "io" "os/exec" "strconv" @@ -29,7 +28,7 @@ type Commit struct { Signature *CommitSignature Parents []ObjectID // ID strings - submoduleCache *ObjectCache + submoduleCache *ObjectCache[*SubModule] } // CommitSignature represents a git commit signature part. @@ -357,69 +356,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { return string(bytes), nil } -// GetSubModules get all the sub modules of current revision git tree -func (c *Commit) GetSubModules() (*ObjectCache, error) { - if c.submoduleCache != nil { - return c.submoduleCache, nil - } - - entry, err := c.GetTreeEntryByPath(".gitmodules") - if err != nil { - if _, ok := err.(ErrNotExist); ok { - return nil, nil - } - return nil, err - } - - rd, err := entry.Blob().DataAsync() - if err != nil { - return nil, err - } - - defer rd.Close() - scanner := bufio.NewScanner(rd) - c.submoduleCache = newObjectCache() - var ismodule bool - var path string - for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "[submodule") { - ismodule = true - continue - } - if ismodule { - fields := strings.Split(scanner.Text(), "=") - k := strings.TrimSpace(fields[0]) - if k == "path" { - path = strings.TrimSpace(fields[1]) - } else if k == "url" { - c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])}) - ismodule = false - } - } - } - if err = scanner.Err(); err != nil { - return nil, fmt.Errorf("GetSubModules scan: %w", err) - } - - return c.submoduleCache, nil -} - -// GetSubModule get the sub module according entryname -func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { - modules, err := c.GetSubModules() - if err != nil { - return nil, err - } - - if modules != nil { - module, has := modules.Get(entryname) - if has { - return module.(*SubModule), nil - } - } - return nil, nil -} - // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') func (c *Commit) GetBranchName() (string, error) { cmd := NewCommand(c.repo.Ctx, "name-rev") diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index c740a4e13eb..545081275b7 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,5 +7,5 @@ package git type CommitInfo struct { Entry *TreeEntry Commit *Commit - SubModuleFile *SubModuleFile + SubModuleFile *CommitSubModuleFile } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 31ffc9aec1c..11b44f7c356 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -71,7 +71,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath commitsInfo[i].Commit = entryCommit } - // If the entry if a submodule add a submodule file for this + // If the entry is a submodule add a submodule file for this if entry.IsSubModule() { subModuleURL := "" var fullPath string @@ -85,7 +85,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) + subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) commitsInfo[i].SubModuleFile = subModuleFile } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index cfde64a0339..20d586f0ff5 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -79,7 +79,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) + subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) commitsInfo[i].SubModuleFile = subModuleFile } } diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go new file mode 100644 index 00000000000..6603061da29 --- /dev/null +++ b/modules/git/commit_submodule.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +// GetSubModules get all the submodules of current revision git tree +func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { + if c.submoduleCache != nil { + return c.submoduleCache, nil + } + + entry, err := c.GetTreeEntryByPath(".gitmodules") + if err != nil { + if _, ok := err.(ErrNotExist); ok { + return nil, nil + } + return nil, err + } + + rd, err := entry.Blob().DataAsync() + if err != nil { + return nil, err + } + defer rd.Close() + + // at the moment we do not strictly limit the size of the .gitmodules file because some users would have huge .gitmodules files (>1MB) + c.submoduleCache, err = configParseSubModules(rd) + if err != nil { + return nil, err + } + return c.submoduleCache, nil +} + +// GetSubModule get the submodule according entry name +func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { + modules, err := c.GetSubModules() + if err != nil { + return nil, err + } + + if modules != nil { + if module, has := modules.Get(entryName); has { + return module, nil + } + } + return nil, nil +} diff --git a/modules/git/submodule.go b/modules/git/commit_submodule_file.go similarity index 83% rename from modules/git/submodule.go rename to modules/git/commit_submodule_file.go index b99c81582b7..bdec35f6829 100644 --- a/modules/git/submodule.go +++ b/modules/git/commit_submodule_file.go @@ -15,24 +15,15 @@ import ( var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) -// SubModule submodule is a reference on git repository -type SubModule struct { - Name string - URL string -} - -// SubModuleFile represents a file with submodule type. -type SubModuleFile struct { - *Commit - +// CommitSubModuleFile represents a file with submodule type. +type CommitSubModuleFile struct { refURL string refID string } -// NewSubModuleFile create a new submodule file -func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile { - return &SubModuleFile{ - Commit: c, +// NewCommitSubModuleFile create a new submodule file +func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile { + return &CommitSubModuleFile{ refURL: refURL, refID: refID, } @@ -109,11 +100,12 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { } // RefURL guesses and returns reference URL. -func (sf *SubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { +// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore) +func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) } // RefID returns reference ID. -func (sf *SubModuleFile) RefID() string { +func (sf *CommitSubModuleFile) RefID() string { return sf.refID } diff --git a/modules/git/submodule_test.go b/modules/git/commit_submodule_file_test.go similarity index 97% rename from modules/git/submodule_test.go rename to modules/git/commit_submodule_file_test.go index e05f2510c4c..473b996b820 100644 --- a/modules/git/submodule_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetRefURL(t *testing.T) { +func TestCommitSubModuleFileGetRefURL(t *testing.T) { kases := []struct { refURL string prefixURL string diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 0ddeb182ef8..bf381a53501 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -135,7 +135,7 @@ author KN4CK3R 1711702962 +0100 committer KN4CK3R 1711702962 +0100 encoding ISO-8859-1 gpgsig -----BEGIN PGP SIGNATURE----- - + iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq @@ -150,7 +150,7 @@ gpgsig -----BEGIN PGP SIGNATURE----- -----END PGP SIGNATURE----- ISO-8859-1` - + commitString = strings.ReplaceAll(commitString, "", " ") sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) diff --git a/modules/git/config.go b/modules/git/config.go new file mode 100644 index 00000000000..9c36cf16540 --- /dev/null +++ b/modules/git/config.go @@ -0,0 +1,187 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" + "os" + "regexp" + "runtime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) +func syncGitConfig() (err error) { + if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { + return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err) + } + + // first, write user's git config options to git config file + // user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes + for k, v := range setting.GitConfig.Options { + if err = configSet(strings.ToLower(k), v); err != nil { + return err + } + } + + // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults" + // TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used. + // If these values are not really used, then they can be set (overwritten) directly without considering about existence. + for configKey, defaultValue := range map[string]string{ + "user.name": "Gitea", + "user.email": "gitea@fake.local", + } { + if err := configSetNonExist(configKey, defaultValue); err != nil { + return err + } + } + + // Set git some configurations - these must be set to these values for gitea to work correctly + if err := configSet("core.quotePath", "false"); err != nil { + return err + } + + if DefaultFeatures().CheckVersionAtLeast("2.10") { + if err := configSet("receive.advertisePushOptions", "true"); err != nil { + return err + } + } + + if DefaultFeatures().CheckVersionAtLeast("2.18") { + if err := configSet("core.commitGraph", "true"); err != nil { + return err + } + if err := configSet("gc.writeCommitGraph", "true"); err != nil { + return err + } + if err := configSet("fetch.writeCommitGraph", "true"); err != nil { + return err + } + } + + if DefaultFeatures().SupportProcReceive { + // set support for AGit flow + if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + } else { + if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + } + + // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. + // However, some docker users and samba users find it difficult to configure their systems correctly, + // so that Gitea's git repositories are owned by the Gitea user. + // (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) + // See issue: https://github.com/go-gitea/gitea/issues/19455 + // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, + // it is now safe to set "safe.directory=*" for internal usage only. + // Although this setting is only supported by some new git versions, it is also tolerated by earlier versions + if err := configAddNonExist("safe.directory", "*"); err != nil { + return err + } + + if runtime.GOOS == "windows" { + if err := configSet("core.longpaths", "true"); err != nil { + return err + } + if setting.Git.DisableCoreProtectNTFS { + err = configSet("core.protectNTFS", "false") + } else { + err = configUnsetAll("core.protectNTFS", "false") + } + if err != nil { + return err + } + } + + // By default partial clones are disabled, enable them from git v2.22 + if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") { + if err = configSet("uploadpack.allowfilter", "true"); err != nil { + return err + } + err = configSet("uploadpack.allowAnySHA1InWant", "true") + } else { + if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil { + return err + } + err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true") + } + + return err +} + +func configSet(key, value string) error { + stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err != nil && !IsErrorExitCode(err, 1) { + return fmt.Errorf("failed to get git config %s, err: %w", key, err) + } + + currValue := strings.TrimSpace(stdout) + if currValue == value { + return nil + } + + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to set git global config %s, err: %w", key, err) + } + + return nil +} + +func configSetNonExist(key, value string) error { + _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err == nil { + // already exist + return nil + } + if IsErrorExitCode(err, 1) { + // not exist, set new config + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to set git global config %s, err: %w", key, err) + } + return nil + } + + return fmt.Errorf("failed to get git config %s, err: %w", key, err) +} + +func configAddNonExist(key, value string) error { + _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + if err == nil { + // already exist + return nil + } + if IsErrorExitCode(err, 1) { + // not exist, add new config + _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to add git global config %s, err: %w", key, err) + } + return nil + } + return fmt.Errorf("failed to get git config %s, err: %w", key, err) +} + +func configUnsetAll(key, value string) error { + _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err == nil { + // exist, need to remove + _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) + } + return nil + } + if IsErrorExitCode(err, 1) { + // not exist + return nil + } + return fmt.Errorf("failed to get git config %s, err: %w", key, err) +} diff --git a/modules/git/config_submodule.go b/modules/git/config_submodule.go new file mode 100644 index 00000000000..fe332088507 --- /dev/null +++ b/modules/git/config_submodule.go @@ -0,0 +1,75 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// SubModule is a reference on git repository +type SubModule struct { + Path string + URL string + Branch string // this field is newly added but not really used +} + +// configParseSubModules this is not a complete parse for gitmodules file, it only +// parses the url and path of submodules. At the moment it only parses well-formed gitmodules files. +// In the future, there should be a complete implementation of https://git-scm.com/docs/git-config#_syntax +func configParseSubModules(r io.Reader) (*ObjectCache[*SubModule], error) { + var subModule *SubModule + subModules := newObjectCache[*SubModule]() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { + continue + } + + // Section header [section] + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + if subModule != nil { + subModules.Set(subModule.Path, subModule) + } + if strings.HasPrefix(line, "[submodule") { + subModule = &SubModule{} + } else { + subModule = nil + } + continue + } + + if subModule == nil { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + switch key { + case "path": + subModule.Path = value + case "url": + subModule.URL = value + case "branch": + subModule.Branch = value + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + if subModule != nil { + subModules.Set(subModule.Path, subModule) + } + return subModules, nil +} diff --git a/modules/git/config_submodule_test.go b/modules/git/config_submodule_test.go new file mode 100644 index 00000000000..f0846c7bfb7 --- /dev/null +++ b/modules/git/config_submodule_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigSubmodule(t *testing.T) { + input := ` +[core] +path = test + +[submodule "submodule1"] + path = path1 + url = https://gitea.io/foo/foo + #branch = b1 + +[other1] +branch = master + +[submodule "submodule2"] + path = path2 + url = https://gitea.io/bar/bar + branch = b2 + +[other2] +branch = main + +[submodule "submodule3"] + path = path3 + url = https://gitea.io/xxx/xxx +` + + subModules, err := configParseSubModules(strings.NewReader(input)) + assert.NoError(t, err) + assert.Len(t, subModules.cache, 3) + + sm1, _ := subModules.Get("path1") + assert.Equal(t, &SubModule{Path: "path1", URL: "https://gitea.io/foo/foo", Branch: ""}, sm1) + sm2, _ := subModules.Get("path2") + assert.Equal(t, &SubModule{Path: "path2", URL: "https://gitea.io/bar/bar", Branch: "b2"}, sm2) + sm3, _ := subModules.Get("path3") + assert.Equal(t, &SubModule{Path: "path3", URL: "https://gitea.io/xxx/xxx", Branch: ""}, sm3) +} diff --git a/modules/git/config_test.go b/modules/git/config_test.go new file mode 100644 index 00000000000..59f70c99e2f --- /dev/null +++ b/modules/git/config_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "os" + "strings" + "testing" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func gitConfigContains(sub string) bool { + if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil { + return strings.Contains(string(b), sub) + } + return false +} + +func TestGitConfig(t *testing.T) { + assert.False(t, gitConfigContains("key-a")) + + assert.NoError(t, configSetNonExist("test.key-a", "val-a")) + assert.True(t, gitConfigContains("key-a = val-a")) + + assert.NoError(t, configSetNonExist("test.key-a", "val-a-changed")) + assert.False(t, gitConfigContains("key-a = val-a-changed")) + + assert.NoError(t, configSet("test.key-a", "val-a-changed")) + assert.True(t, gitConfigContains("key-a = val-a-changed")) + + assert.NoError(t, configAddNonExist("test.key-b", "val-b")) + assert.True(t, gitConfigContains("key-b = val-b")) + + assert.NoError(t, configAddNonExist("test.key-b", "val-2b")) + assert.True(t, gitConfigContains("key-b = val-b")) + assert.True(t, gitConfigContains("key-b = val-2b")) + + assert.NoError(t, configUnsetAll("test.key-b", "val-b")) + assert.False(t, gitConfigContains("key-b = val-b")) + assert.True(t, gitConfigContains("key-b = val-2b")) + + assert.NoError(t, configUnsetAll("test.key-b", "val-2b")) + assert.False(t, gitConfigContains("key-b = val-2b")) + + assert.NoError(t, configSet("test.key-x", "*")) + assert.True(t, gitConfigContains("key-x = *")) + assert.NoError(t, configSetNonExist("test.key-x", "*")) + assert.NoError(t, configUnsetAll("test.key-x", "*")) + assert.False(t, gitConfigContains("key-x = *")) +} + +func TestSyncConfig(t *testing.T) { + oldGitConfig := setting.GitConfig + defer func() { + setting.GitConfig = oldGitConfig + }() + + setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" + assert.NoError(t, syncGitConfig()) + assert.True(t, gitConfigContains("[sync-test]")) + assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) +} diff --git a/modules/git/fsck.go b/modules/git/fsck.go new file mode 100644 index 00000000000..cec27f165b9 --- /dev/null +++ b/modules/git/fsck.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "time" +) + +// Fsck verifies the connectivity and validity of the objects in the database +func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error { + return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) +} diff --git a/modules/git/git.go b/modules/git/git.go index a19dd7771ba..e3e5b832747 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "runtime" "strings" "time" @@ -95,17 +94,18 @@ func parseGitVersionLine(s string) (*version.Version, error) { return version.NewVersion(versionString) } -// SetExecutablePath changes the path of git executable and checks the file permission and version. -func SetExecutablePath(path string) error { - // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. - if path != "" { - GitExecutable = path +func checkGitVersionCompatibility(gitVer *version.Version) error { + badVersions := []struct { + Version *version.Version + Reason string + }{ + {version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"}, } - absPath, err := exec.LookPath(GitExecutable) - if err != nil { - return fmt.Errorf("git not found: %w", err) + for _, bad := range badVersions { + if gitVer.Equal(bad.Version) { + return errors.New(bad.Reason) + } } - GitExecutable = absPath return nil } @@ -128,6 +128,20 @@ func ensureGitVersion() error { return nil } +// SetExecutablePath changes the path of git executable and checks the file permission and version. +func SetExecutablePath(path string) error { + // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. + if path != "" { + GitExecutable = path + } + absPath, err := exec.LookPath(GitExecutable) + if err != nil { + return fmt.Errorf("git not found: %w", err) + } + GitExecutable = absPath + return nil +} + // HomeDir is the home dir for git to store the global config file used by Gitea internally func HomeDir() string { if setting.Git.HomePath == "" { @@ -204,196 +218,3 @@ func InitFull(ctx context.Context) (err error) { return syncGitConfig() } - -// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) -func syncGitConfig() (err error) { - if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { - return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err) - } - - // first, write user's git config options to git config file - // user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes - for k, v := range setting.GitConfig.Options { - if err = configSet(strings.ToLower(k), v); err != nil { - return err - } - } - - // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults" - // TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used. - // If these values are not really used, then they can be set (overwritten) directly without considering about existence. - for configKey, defaultValue := range map[string]string{ - "user.name": "Gitea", - "user.email": "gitea@fake.local", - } { - if err := configSetNonExist(configKey, defaultValue); err != nil { - return err - } - } - - // Set git some configurations - these must be set to these values for gitea to work correctly - if err := configSet("core.quotePath", "false"); err != nil { - return err - } - - if DefaultFeatures().CheckVersionAtLeast("2.10") { - if err := configSet("receive.advertisePushOptions", "true"); err != nil { - return err - } - } - - if DefaultFeatures().CheckVersionAtLeast("2.18") { - if err := configSet("core.commitGraph", "true"); err != nil { - return err - } - if err := configSet("gc.writeCommitGraph", "true"); err != nil { - return err - } - if err := configSet("fetch.writeCommitGraph", "true"); err != nil { - return err - } - } - - if DefaultFeatures().SupportProcReceive { - // set support for AGit flow - if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { - return err - } - } else { - if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { - return err - } - } - - // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. - // However, some docker users and samba users find it difficult to configure their systems correctly, - // so that Gitea's git repositories are owned by the Gitea user. - // (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) - // See issue: https://github.com/go-gitea/gitea/issues/19455 - // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, - // it is now safe to set "safe.directory=*" for internal usage only. - // Although this setting is only supported by some new git versions, it is also tolerated by earlier versions - if err := configAddNonExist("safe.directory", "*"); err != nil { - return err - } - - if runtime.GOOS == "windows" { - if err := configSet("core.longpaths", "true"); err != nil { - return err - } - if setting.Git.DisableCoreProtectNTFS { - err = configSet("core.protectNTFS", "false") - } else { - err = configUnsetAll("core.protectNTFS", "false") - } - if err != nil { - return err - } - } - - // By default partial clones are disabled, enable them from git v2.22 - if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") { - if err = configSet("uploadpack.allowfilter", "true"); err != nil { - return err - } - err = configSet("uploadpack.allowAnySHA1InWant", "true") - } else { - if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil { - return err - } - err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true") - } - - return err -} - -func checkGitVersionCompatibility(gitVer *version.Version) error { - badVersions := []struct { - Version *version.Version - Reason string - }{ - {version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"}, - } - for _, bad := range badVersions { - if gitVer.Equal(bad.Version) { - return errors.New(bad.Reason) - } - } - return nil -} - -func configSet(key, value string) error { - stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err != nil && !IsErrorExitCode(err, 1) { - return fmt.Errorf("failed to get git config %s, err: %w", key, err) - } - - currValue := strings.TrimSpace(stdout) - if currValue == value { - return nil - } - - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to set git global config %s, err: %w", key, err) - } - - return nil -} - -func configSetNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err == nil { - // already exist - return nil - } - if IsErrorExitCode(err, 1) { - // not exist, set new config - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to set git global config %s, err: %w", key, err) - } - return nil - } - - return fmt.Errorf("failed to get git config %s, err: %w", key, err) -} - -func configAddNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) - if err == nil { - // already exist - return nil - } - if IsErrorExitCode(err, 1) { - // not exist, add new config - _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to add git global config %s, err: %w", key, err) - } - return nil - } - return fmt.Errorf("failed to get git config %s, err: %w", key, err) -} - -func configUnsetAll(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err == nil { - // exist, need to remove - _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) - } - return nil - } - if IsErrorExitCode(err, 1) { - // not exist - return nil - } - return fmt.Errorf("failed to get git config %s, err: %w", key, err) -} - -// Fsck verifies the connectivity and validity of the objects in the database -func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error { - return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) -} diff --git a/modules/git/git_test.go b/modules/git/git_test.go index fc92bebe047..5472842b76e 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "os" - "strings" "testing" "code.gitea.io/gitea/modules/setting" @@ -43,58 +42,6 @@ func TestMain(m *testing.M) { } } -func gitConfigContains(sub string) bool { - if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil { - return strings.Contains(string(b), sub) - } - return false -} - -func TestGitConfig(t *testing.T) { - assert.False(t, gitConfigContains("key-a")) - - assert.NoError(t, configSetNonExist("test.key-a", "val-a")) - assert.True(t, gitConfigContains("key-a = val-a")) - - assert.NoError(t, configSetNonExist("test.key-a", "val-a-changed")) - assert.False(t, gitConfigContains("key-a = val-a-changed")) - - assert.NoError(t, configSet("test.key-a", "val-a-changed")) - assert.True(t, gitConfigContains("key-a = val-a-changed")) - - assert.NoError(t, configAddNonExist("test.key-b", "val-b")) - assert.True(t, gitConfigContains("key-b = val-b")) - - assert.NoError(t, configAddNonExist("test.key-b", "val-2b")) - assert.True(t, gitConfigContains("key-b = val-b")) - assert.True(t, gitConfigContains("key-b = val-2b")) - - assert.NoError(t, configUnsetAll("test.key-b", "val-b")) - assert.False(t, gitConfigContains("key-b = val-b")) - assert.True(t, gitConfigContains("key-b = val-2b")) - - assert.NoError(t, configUnsetAll("test.key-b", "val-2b")) - assert.False(t, gitConfigContains("key-b = val-2b")) - - assert.NoError(t, configSet("test.key-x", "*")) - assert.True(t, gitConfigContains("key-x = *")) - assert.NoError(t, configSetNonExist("test.key-x", "*")) - assert.NoError(t, configUnsetAll("test.key-x", "*")) - assert.False(t, gitConfigContains("key-x = *")) -} - -func TestSyncConfig(t *testing.T) { - oldGitConfig := setting.GitConfig - defer func() { - setting.GitConfig = oldGitConfig - }() - - setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" - assert.NoError(t, syncGitConfig()) - assert.True(t, gitConfigContains("[sync-test]")) - assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) -} - func TestParseGitVersion(t *testing.T) { v, err := parseGitVersionLine("git version 2.29.3") assert.NoError(t, err) diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index a1127f4e6c5..0ca1ea79c21 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -28,7 +28,7 @@ const isGogit = true type Repository struct { Path string - tagCache *ObjectCache + tagCache *ObjectCache[*Tag] gogitRepo *gogit.Repository gogitStorage *filesystem.Storage @@ -79,7 +79,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { Path: repoPath, gogitRepo: gogitRepo, gogitStorage: storage, - tagCache: newObjectCache(), + tagCache: newObjectCache[*Tag](), Ctx: ctx, objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(), }, nil diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 3eb2e2ee6b2..477e3b87429 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -21,7 +21,7 @@ const isGogit = false type Repository struct { Path string - tagCache *ObjectCache + tagCache *ObjectCache[*Tag] gpgSettings *GPGSettings @@ -53,7 +53,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { return &Repository{ Path: repoPath, - tagCache: newObjectCache(), + tagCache: newObjectCache[*Tag](), Ctx: ctx, }, nil } diff --git a/modules/git/repo_tag_gogit.go b/modules/git/repo_tag_gogit.go index 4a7a06e9bda..3e1b4e89ad6 100644 --- a/modules/git/repo_tag_gogit.go +++ b/modules/git/repo_tag_gogit.go @@ -72,7 +72,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) - tagClone := *t.(*Tag) + tagClone := *t tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil } diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 8b06a6a1c3c..e0a3104249e 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -51,7 +51,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) - tagClone := *t.(*Tag) + tagClone := *t tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil } diff --git a/modules/git/utils.go b/modules/git/utils.go index 53211c64518..56cba9087a3 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -15,27 +15,25 @@ import ( ) // ObjectCache provides thread-safe cache operations. -type ObjectCache struct { +type ObjectCache[T any] struct { lock sync.RWMutex - cache map[string]any + cache map[string]T } -func newObjectCache() *ObjectCache { - return &ObjectCache{ - cache: make(map[string]any, 10), - } +func newObjectCache[T any]() *ObjectCache[T] { + return &ObjectCache[T]{cache: make(map[string]T, 10)} } -// Set add obj to cache -func (oc *ObjectCache) Set(id string, obj any) { +// Set adds obj to cache +func (oc *ObjectCache[T]) Set(id string, obj T) { oc.lock.Lock() defer oc.lock.Unlock() oc.cache[id] = obj } -// Get get cached obj by id -func (oc *ObjectCache) Get(id string) (any, bool) { +// Get gets cached obj by id +func (oc *ObjectCache[T]) Get(id string) (T, bool) { oc.lock.RLock() defer oc.lock.RUnlock() diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index 248673a907d..3afc116e682 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -4,8 +4,6 @@ package repository import ( - "crypto/md5" - "fmt" "strconv" "testing" "time" @@ -125,15 +123,12 @@ func TestPushCommits_AvatarLink(t *testing.T) { }, } - setting.GravatarSource = "https://secure.gravatar.com/avatar" - setting.OfflineMode = true - assert.Equal(t, - "/avatars/avatar2?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), + "/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), pushCommits.AvatarLink(db.DefaultContext, "user2@example.com")) assert.Equal(t, - fmt.Sprintf("https://secure.gravatar.com/avatar/%x?d=identicon&s=%d", md5.Sum([]byte("nonexistent@example.com")), 28*setting.Avatar.RenderedSizeFactor), + "/assets/img/avatar_default.png", pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com")) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7e0d280ec10..3ecdfa42f72 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -104,6 +104,7 @@ copy_url = Copy URL copy_hash = Copy hash copy_content = Copy content copy_branch = Copy branch name +copy_path = Copy path copy_success = Copied! copy_error = Copy failed copy_type_unsupported = This file type cannot be copied @@ -352,6 +353,7 @@ enable_update_checker = Enable Update Checker enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. env_config_keys = Environment Configuration env_config_keys_prompt = The following environment variables will also be applied to your configuration file: +config_write_file_prompt = These configuration options will be written into: %s [home] nav_menu = Navigation Menu diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index bb16858c811..1cea7d8c72e 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -133,11 +133,6 @@ func DeleteBranch(ctx *context.APIContext) { branchName := ctx.PathParam("*") - if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusForbidden, "", "Git Repository is empty.") - return - } - // check whether branches of this repository has been synced totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{ RepoID: ctx.Repo.Repository.ID, diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index bb814eab6e7..cd20c0b18e8 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -348,6 +348,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } if !baseGitRepo.IsBranchExist(pull.BaseBranch) { + ctx.Data["BaseBranchNotExist"] = true ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["HeadTarget"] = pull.HeadBranch diff --git a/routers/web/web.go b/routers/web/web.go index 8cd4455899d..2995de8d95f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -561,7 +561,7 @@ func registerRoutes(m *web.Router) { m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) }, optSignInIgnoreCsrf, reqSignIn) - m.Methods("GET, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) + m.Methods("GET, POST, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) m.Methods("POST, OPTIONS", "/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth) m.Methods("GET, OPTIONS", "/keys", optionsCorsHandler(), optSignInIgnoreCsrf, auth.OIDCKeys) m.Methods("POST, OPTIONS", "/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), optSignInIgnoreCsrf, auth.IntrospectOAuth) diff --git a/services/actions/auth.go b/services/actions/auth.go index 8e934d89a84..1ef21f6e0eb 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { return 0, fmt.Errorf("split token failed") } - token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) { + return TokenToTaskID(parts[1]) +} + +// TokenToTaskID returns the TaskID associated with the provided JWT token +func TokenToTaskID(token string) (int64, error) { + parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } @@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) { return 0, err } - c, ok := token.Claims.(*actionsClaims) - if !token.Valid || !ok { + c, ok := parsedToken.Claims.(*actionsClaims) + if !parsedToken.Valid || !ok { return 0, fmt.Errorf("invalid token claim") } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index d0aec085b10..251ae5a244b 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/oauth2_provider" ) @@ -54,6 +55,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 { return grant.UserID } +// CheckTaskIsRunning verifies that the TaskID corresponds to a running task +func CheckTaskIsRunning(ctx context.Context, taskID int64) bool { + // Verify the task exists + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + return false + } + + // Verify that it's running + return task.Status == actions_model.StatusRunning +} + // OAuth2 implements the Auth interface and authenticates requests // (API requests only) by looking for an OAuth token in query parameters or the // "Authorization" header. @@ -97,6 +110,16 @@ func parseToken(req *http.Request) (string, bool) { func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 { // Let's see if token is valid. if strings.Contains(tokenSHA, ".") { + // First attempt to decode an actions JWT, returning the actions user + if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil { + if CheckTaskIsRunning(ctx, taskID) { + store.GetData()["IsActionsToken"] = true + store.GetData()["ActionsTaskID"] = taskID + return user_model.ActionsUserID + } + } + + // Otherwise, check if this is an OAuth access token uid := CheckOAuthAccessToken(ctx, tokenSHA) if uid != 0 { store.GetData()["IsApiToken"] = true diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go new file mode 100644 index 00000000000..75c231ff7a4 --- /dev/null +++ b/services/auth/oauth2_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "context" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/actions" + + "github.com/stretchr/testify/assert" +) + +func TestUserIDFromToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Actions JWT", func(t *testing.T) { + const RunningTaskID = 47 + token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) + assert.NoError(t, err) + + ds := make(middleware.ContextData) + + o := OAuth2{} + uid := o.userIDFromToken(context.Background(), token, ds) + assert.Equal(t, int64(user_model.ActionsUserID), uid) + assert.Equal(t, ds["IsActionsToken"], true) + assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) + }) +} + +func TestCheckTaskIsRunning(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + cases := map[string]struct { + TaskID int64 + Expected bool + }{ + "Running": {TaskID: 47, Expected: true}, + "Missing": {TaskID: 1, Expected: false}, + "Cancelled": {TaskID: 46, Expected: false}, + } + + for name := range cases { + c := cases[name] + t.Run(name, func(t *testing.T) { + actual := CheckTaskIsRunning(context.Background(), c.TaskID) + assert.Equal(t, c.Expected, actual) + }) + } +} diff --git a/services/context/repo.go b/services/context/repo.go index e7b32d62832..1eafb7ca48b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -393,14 +393,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } } - pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{}) - if err != nil { - ctx.ServerError("GetPushMirrorsByRepoID", err) - return - } - ctx.Repo.Repository = repo - ctx.Data["PushMirrors"] = pushMirrors ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go index f22c115276e..6db93f6a64d 100644 --- a/services/repository/contributors_graph_test.go +++ b/services/repository/contributors_graph_test.go @@ -18,6 +18,7 @@ import ( func TestRepository_ContributorsGraph(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) assert.NoError(t, repo.LoadOwner(db.DefaultContext)) mockCache, err := cache.NewStringCache(setting.Cache{}) @@ -46,7 +47,7 @@ func TestRepository_ContributorsGraph(t *testing.T) { assert.EqualValues(t, &ContributorData{ Name: "Ethan Koenig", - AvatarLink: "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon", + AvatarLink: "/assets/img/avatar_default.png", TotalCommits: 1, Weeks: map[int64]*WeekData{ 1511654400000: { diff --git a/templates/install.tmpl b/templates/install.tmpl index 5055031a902..6c4cc7df01b 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -338,7 +338,9 @@
- These configuration options will be written into: {{.CustomConfFile}} + {{$copyBtn := svg "octicon-copy" 14}} + {{$filePath := HTMLFormat `%s ` .CustomConfFile .CustomConfFile $copyBtn}} + {{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 2f9d4ecab6f..26737f110e4 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -130,7 +130,7 @@
{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}} {{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}} - + {{if $file.IsGenerated}} {{ctx.Locale.Tr "repo.diff.generated"}} {{end}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 7a6cbbc4e25..0a8391b5535 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -106,6 +106,7 @@ / {{- if eq $i $l -}} {{$v}} + {{- else -}} {{$p := index $.Paths $i}}{{$v}} {{- end -}} diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl index bee6123e52c..d8ccd73387d 100644 --- a/templates/repo/issue/sidebar/assignee_list.tmpl +++ b/templates/repo/issue/sidebar/assignee_list.tmpl @@ -16,12 +16,14 @@
{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}
- {{range $data.CandidateAssignees}} - - {{svg "octicon-check"}} - {{ctx.AvatarUtils.Avatar . 20}} {{template "repo/search_name" .}} - - {{end}} +
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index ed80047661b..fb8f1a667e7 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -17,25 +17,27 @@
{{ctx.Locale.Tr "repo.issues.new.clear_labels"}} - {{$previousExclusiveScope := "_no_scope"}} - {{range $data.RepoLabels}} - {{$exclusiveScope := .ExclusiveScope}} - {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} -
+ {{end}} diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl index 4f2b4cb06fb..4a7f3f8ad88 100644 --- a/templates/repo/issue/sidebar/milestone_list.tmpl +++ b/templates/repo/issue/sidebar/milestone_list.tmpl @@ -20,25 +20,26 @@
{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}
- {{if $data.OpenMilestones}} -
-
{{ctx.Locale.Tr "repo.issues.new.open_milestone"}}
- {{range $data.OpenMilestones}} - - {{svg "octicon-milestone" 18}} {{.Name}} - + {{end}} - {{end}} diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl index ab1243caddc..0cbbdce7608 100644 --- a/templates/repo/issue/sidebar/project_list.tmpl +++ b/templates/repo/issue/sidebar/project_list.tmpl @@ -18,24 +18,25 @@ {{end}}
{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}
- {{if $data.OpenProjects}} -
-
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
- {{range $data.OpenProjects}} - - {{svg .IconName 18}} {{.Title}} - +
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl index e990fc5afc8..b3d9590d585 100644 --- a/templates/repo/issue/sidebar/reviewer_list.tmpl +++ b/templates/repo/issue/sidebar/reviewer_list.tmpl @@ -17,27 +17,29 @@
{{end}} - {{range $data.Reviewers}} - {{if .User}} - - {{svg "octicon-check"}} - {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}} - - {{end}} - {{end}} - {{if $data.TeamReviewers}} - {{if $data.Reviewers}}
{{end}} - {{range $data.TeamReviewers}} - {{if .Team}} - + diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 477b6b33c6a..47551c86e47 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -231,7 +231,8 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}} + {{$oldRef := HTMLFormat `%s` .OldRef}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" $oldRef $createdStr}} {{else if eq .Type 12}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 26b36d6ffcf..cf775f22b57 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -61,12 +61,16 @@ {{if .Issue.PullRequest.IsAgitFlow}} {{$headHref = HTMLFormat `%s AGit` $headHref "https://docs.gitea.com/usage/agit" (ctx.Locale.Tr "repo.pull.agit_documentation")}} {{else}} - {{$headHref = HTMLFormat `%s` (ctx.Locale.Tr "form.target_branch_not_exist") $headHref}} + {{$headHref = HTMLFormat `%s` (ctx.Locale.Tr "form.target_branch_not_exist") $headHref}} {{end}} {{end}} {{$baseHref := .BaseTarget}} {{if .BaseBranchLink}} - {{$baseHref = HTMLFormat `%s` .BaseBranchLink $baseHref}} + {{if .BaseBranchNotExist}} + {{$baseHref = HTMLFormat `%s` (ctx.Locale.Tr "form.target_branch_not_exist") $baseHref}} + {{else}} + {{$baseHref = HTMLFormat `%s` .BaseBranchLink $baseHref}} + {{end}} {{end}} {{if .Issue.PullRequest.HasMerged}} {{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index d6d27e66be1..78e42777bb2 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -107,7 +107,7 @@ func TestE2e(t *testing.T) { cmd.Stdout = &stdout cmd.Stderr = &stderr - err := cmd.Run() + err = cmd.Run() if err != nil { // Currently colored output is conflicting. Using Printf until that is resolved. fmt.Printf("%v", stdout.String()) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 2351e169bc5..bbd99f7aab0 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -589,7 +589,8 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - targetBranch := htmlDoc.doc.Find("#branch_target>a").Text() + // the branch has been deleted, so there is no a html tag instead of span + targetBranch := htmlDoc.doc.Find("#branch_target>span").Text() prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text()) assert.EqualValues(t, "base-pr", targetBranch) diff --git a/web_src/css/base.css b/web_src/css/base.css index b5a39c7af6f..babbf4c89dc 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1397,6 +1397,10 @@ table th[data-sortt-desc] .svg { gap: .5rem; min-width: 0; } +.ui.dropdown .menu.flex-items-menu > .item img, +.ui.dropdown .menu.flex-items-menu > .item svg { + margin: 0; /* use gap, but not margin */ +} .ui.dropdown.ellipsis-items-nowrap > .text { overflow: hidden; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 01ddab97e59..12cdc4657bf 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -53,11 +53,6 @@ .issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark { visibility: hidden; } -/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */ -.issue-sidebar-combo .ui.dropdown .menu > .item > img, -.issue-sidebar-combo .ui.dropdown .menu > .item > svg { - margin: 0; -} .issue-content-right .dropdown > .menu { max-width: 270px; @@ -66,6 +61,12 @@ overflow-x: auto; } +.issue-content-right .dropdown > .menu .item-secondary-info small { + display: block; + text-overflow: ellipsis; + overflow: hidden; +} + @media (max-width: 767.98px) { .issue-content-left, .issue-content-right { @@ -73,11 +74,6 @@ } } -.repository .issue-content-right .filter.menu { - max-height: 500px; - overflow-x: auto; -} - .repository .filter.menu.labels .label-filter .menu .info { display: inline-block; padding: 0.5rem 0; diff --git a/web_src/js/features/autofocus-end.ts b/web_src/js/features/autofocus-end.ts index da71ce9536d..53e475b543d 100644 --- a/web_src/js/features/autofocus-end.ts +++ b/web_src/js/features/autofocus-end.ts @@ -1,5 +1,5 @@ export function initAutoFocusEnd() { - for (const el of document.querySelectorAll('.js-autofocus-end')) { + for (const el of document.querySelectorAll('.js-autofocus-end')) { el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. el.setSelectionRange(el.value.length, el.value.length); } diff --git a/web_src/js/features/captcha.ts b/web_src/js/features/captcha.ts index 23dbae37406..69b4aa6852f 100644 --- a/web_src/js/features/captcha.ts +++ b/web_src/js/features/captcha.ts @@ -35,9 +35,11 @@ export async function initCaptcha() { } case 'm-captcha': { const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue'); + // @ts-expect-error mCaptcha.INPUT_NAME = 'm-captcha-response'; const instanceURL = captchaEl.getAttribute('data-instance-url'); + // @ts-expect-error mCaptcha.default({ siteKey: { instanceUrl: new URL(instanceURL), diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index c9b07efe778..8fc6beabfb2 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -3,7 +3,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; const {pageData} = window.config; -async function initInputCitationValue(citationCopyApa, citationCopyBibtex) { +async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { const [{Cite, plugins}] = await Promise.all([ import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), @@ -27,9 +27,9 @@ export async function initCitationFileCopyContent() { if (!pageData.citationFileContent) return; - const citationCopyApa = document.querySelector('#citation-copy-apa'); - const citationCopyBibtex = document.querySelector('#citation-copy-bibtex'); - const inputContent = document.querySelector('#citation-copy-content'); + const citationCopyApa = document.querySelector('#citation-copy-apa'); + const citationCopyBibtex = document.querySelector('#citation-copy-bibtex'); + const inputContent = document.querySelector('#citation-copy-content'); if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return; @@ -41,7 +41,7 @@ export async function initCitationFileCopyContent() { citationCopyApa.classList.toggle('primary', !isBibtex); }; - document.querySelector('#cite-repo-button')?.addEventListener('click', async (e) => { + document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => { const dropdownBtn = e.target.closest('.ui.dropdown.button'); dropdownBtn.classList.add('is-loading'); diff --git a/web_src/js/features/clipboard.ts b/web_src/js/features/clipboard.ts index 8de150d0f98..8f40f34f741 100644 --- a/web_src/js/features/clipboard.ts +++ b/web_src/js/features/clipboard.ts @@ -9,7 +9,7 @@ const {copy_success, copy_error} = window.config.i18n; // - data-clipboard-target: Holds a selector for a or