mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-20 13:06:34 +01:00
Merge branch 'main' into feature/workflow-graph
This commit is contained in:
commit
a6bac394fb
@ -134,7 +134,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
|
||||
}
|
||||
log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
|
||||
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
log.Warn(" SyncReleasesWithTags: %v", err)
|
||||
gitRepo.Close()
|
||||
continue
|
||||
|
||||
@ -342,7 +342,7 @@ export default defineConfig([
|
||||
'import-x/first': [2],
|
||||
'import-x/group-exports': [0],
|
||||
'import-x/max-dependencies': [0],
|
||||
'import-x/named': [2],
|
||||
'import-x/named': [0],
|
||||
'import-x/namespace': [0],
|
||||
'import-x/newline-after-import': [0],
|
||||
'import-x/no-absolute-path': [0],
|
||||
@ -987,7 +987,7 @@ export default defineConfig([
|
||||
'vitest/require-to-throw-message': [0],
|
||||
'vitest/require-top-level-describe': [0],
|
||||
'vitest/valid-describe-callback': [2],
|
||||
'vitest/valid-expect': [2],
|
||||
'vitest/valid-expect': [2, {maxArgs: 2}],
|
||||
'vitest/valid-title': [2],
|
||||
},
|
||||
},
|
||||
|
||||
2
go.mod
2
go.mod
@ -11,7 +11,7 @@ godebug x509negativeserial=1
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.4.1
|
||||
code.gitea.io/sdk/gitea v0.22.0
|
||||
code.gitea.io/sdk/gitea v0.23.2
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||
connectrpc.com/connect v1.19.1
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
|
||||
|
||||
4
go.sum
4
go.sum
@ -20,8 +20,8 @@ code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLr
|
||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
||||
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
|
||||
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
|
||||
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
||||
|
||||
@ -98,6 +98,10 @@ func getChromaLexerByLanguage(fileName, lang string) chroma.Lexer {
|
||||
lang = "C++"
|
||||
}
|
||||
}
|
||||
if lang == "" && util.AsciiEqualFold(ext, ".sql") {
|
||||
// there is a bug when using MySQL lexer: "--\nSELECT", the second line will be rendered as comment incorrectly
|
||||
lang = "SQL"
|
||||
}
|
||||
// lexers.Get is slow if the language name can't be matched directly: it does extra "Match" call to iterate all lexers
|
||||
return lexers.Get(lang)
|
||||
}
|
||||
|
||||
@ -108,6 +108,12 @@ c=2
|
||||
),
|
||||
lexerName: "Python",
|
||||
},
|
||||
{
|
||||
name: "test.sql",
|
||||
code: "--\nSELECT",
|
||||
want: []template.HTML{"<span class=\"c1\">--\n</span>", `<span class="k">SELECT</span>`},
|
||||
lexerName: "SQL",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@ -17,6 +17,13 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// SyncResult describes a reference update detected during sync.
|
||||
type SyncResult struct {
|
||||
RefName git.RefName
|
||||
OldCommitID string
|
||||
NewCommitID string
|
||||
}
|
||||
|
||||
// SyncRepoBranches synchronizes branch table with repository branches
|
||||
func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
@ -33,18 +40,19 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID)
|
||||
count, _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) {
|
||||
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, []*SyncResult, error) {
|
||||
objFmt, err := gitRepo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("GetObjectFormat: %w", err)
|
||||
return 0, nil, fmt.Errorf("GetObjectFormat: %w", err)
|
||||
}
|
||||
if objFmt.Name() != repo.ObjectFormatName {
|
||||
repo.ObjectFormatName = objFmt.Name()
|
||||
if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "object_format_name"); err != nil {
|
||||
return 0, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err)
|
||||
return 0, nil, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +60,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
{
|
||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, nil, err
|
||||
}
|
||||
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
|
||||
for _, branch := range branches {
|
||||
@ -67,7 +75,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, branch := range branches {
|
||||
dbBranches[branch.Name] = branch
|
||||
@ -77,11 +85,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
var toAdd []*git_model.Branch
|
||||
var toUpdate []*git_model.Branch
|
||||
var toRemove []int64
|
||||
var syncResults []*SyncResult
|
||||
for branch := range allBranches {
|
||||
dbb := dbBranches[branch]
|
||||
commit, err := gitRepo.GetBranchCommit(branch)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, nil, err
|
||||
}
|
||||
if dbb == nil {
|
||||
toAdd = append(toAdd, &git_model.Branch{
|
||||
@ -92,7 +101,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
PusherID: doerID,
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
})
|
||||
} else if commit.ID.String() != dbb.CommitID {
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromBranch(branch),
|
||||
OldCommitID: "",
|
||||
NewCommitID: commit.ID.String(),
|
||||
})
|
||||
} else if commit.ID.String() != dbb.CommitID || dbb.IsDeleted {
|
||||
toUpdate = append(toUpdate, &git_model.Branch{
|
||||
ID: dbb.ID,
|
||||
RepoID: repo.ID,
|
||||
@ -102,19 +116,29 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
PusherID: doerID,
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
})
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromBranch(branch),
|
||||
OldCommitID: dbb.CommitID,
|
||||
NewCommitID: commit.ID.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, dbBranch := range dbBranches {
|
||||
if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted {
|
||||
toRemove = append(toRemove, dbBranch.ID)
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromBranch(dbBranch.Name),
|
||||
OldCommitID: dbBranch.CommitID,
|
||||
NewCommitID: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove)
|
||||
|
||||
if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 {
|
||||
return int64(len(allBranches)), nil
|
||||
return int64(len(allBranches)), syncResults, nil
|
||||
}
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
@ -140,7 +164,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
return 0, nil, err
|
||||
}
|
||||
return int64(len(allBranches)), nil
|
||||
return int64(len(allBranches)), syncResults, nil
|
||||
}
|
||||
|
||||
@ -53,7 +53,8 @@ func SyncRepoTags(ctx context.Context, repoID int64) error {
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
return SyncReleasesWithTags(ctx, repo, gitRepo)
|
||||
_, err = SyncReleasesWithTags(ctx, repo, gitRepo)
|
||||
return err
|
||||
}
|
||||
|
||||
// StoreMissingLfsObjectsInRepository downloads missing LFS objects
|
||||
@ -178,13 +179,14 @@ func (shortRelease) TableName() string {
|
||||
// upstream. Hence, after each sync we want the release set to be
|
||||
// identical to the upstream tag set. This is much more efficient for
|
||||
// repositories like https://github.com/vim/vim (with over 13000 tags).
|
||||
func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
|
||||
func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) ([]*SyncResult, error) {
|
||||
log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
|
||||
tags, _, err := gitRepo.GetTagInfos(0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
|
||||
return nil, fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
|
||||
}
|
||||
var added, deleted, updated int
|
||||
var syncResults []*SyncResult
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
@ -195,7 +197,45 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
|
||||
return fmt.Errorf("unable to FindReleases in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
|
||||
}
|
||||
|
||||
dbReleasesByID := make(map[int64]*shortRelease, len(dbReleases))
|
||||
dbReleasesByTag := make(map[string]*shortRelease, len(dbReleases))
|
||||
for _, release := range dbReleases {
|
||||
dbReleasesByID[release.ID] = release
|
||||
dbReleasesByTag[release.TagName] = release
|
||||
}
|
||||
|
||||
inserts, deletes, updates := calcSync(tags, dbReleases)
|
||||
syncResults = make([]*SyncResult, 0, len(inserts)+len(deletes)+len(updates))
|
||||
for _, tag := range inserts {
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromTag(tag.Name),
|
||||
OldCommitID: "",
|
||||
NewCommitID: tag.Object.String(),
|
||||
})
|
||||
}
|
||||
for _, deleteID := range deletes {
|
||||
release := dbReleasesByID[deleteID]
|
||||
if release == nil {
|
||||
continue
|
||||
}
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromTag(release.TagName),
|
||||
OldCommitID: release.Sha1,
|
||||
NewCommitID: "",
|
||||
})
|
||||
}
|
||||
for _, tag := range updates {
|
||||
release := dbReleasesByTag[tag.Name]
|
||||
oldSha := ""
|
||||
if release != nil {
|
||||
oldSha = release.Sha1
|
||||
}
|
||||
syncResults = append(syncResults, &SyncResult{
|
||||
RefName: git.RefNameFromTag(tag.Name),
|
||||
OldCommitID: oldSha,
|
||||
NewCommitID: tag.Object.String(),
|
||||
})
|
||||
}
|
||||
//
|
||||
// make release set identical to upstream tags
|
||||
//
|
||||
@ -238,11 +278,11 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
|
||||
return nil, fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
|
||||
}
|
||||
|
||||
log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated)
|
||||
return nil
|
||||
return syncResults, nil
|
||||
}
|
||||
|
||||
func calcSync(destTags []*git.Tag, dbTags []*shortRelease) ([]*git.Tag, []int64, []*git.Tag) {
|
||||
|
||||
@ -2124,6 +2124,8 @@
|
||||
"repo.settings.pulls.ignore_whitespace": "Déan neamhaird de spás bán le haghaidh coinbhleachtaí",
|
||||
"repo.settings.pulls.enable_autodetect_manual_merge": "Cumasaigh cumasc láimhe autodetector (Nóta: I roinnt cásanna speisialta, is féidir míbhreithiúnais tarlú)",
|
||||
"repo.settings.pulls.allow_rebase_update": "Cumasaigh brainse iarratais tarraingthe a nuashonrú trí athbhunú",
|
||||
"repo.settings.pulls.default_target_branch": "Brainse sprice réamhshocraithe le haghaidh iarratais tarraingthe nua",
|
||||
"repo.settings.pulls.default_target_branch_default": "Brainse réamhshocraithe (%s)",
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "Scrios brainse an iarratais tarraingthe tar éis cumasc de réir réamhshocraithe",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "Ceadaigh eagarthóirí ó chothabhálaí de réir réamhshocraithe",
|
||||
"repo.settings.releases_desc": "Cumasaigh Eisiúintí Stórais",
|
||||
@ -2436,9 +2438,10 @@
|
||||
"repo.settings.block_outdated_branch_desc": "Ní bheidh cumasc indéanta nuair a bhíonn ceannbhrainse taobh thiar de bhronnbhrainse.",
|
||||
"repo.settings.block_admin_merge_override": "Ní mór do riarthóirí rialacha cosanta brainse a leanúint",
|
||||
"repo.settings.block_admin_merge_override_desc": "Ní mór do riarthóirí rialacha cosanta brainse a leanúint agus ní féidir leo iad a sheachaint.",
|
||||
"repo.settings.default_branch_desc": "Roghnaigh brainse stóras réamhshocraithe le haghaidh iarratas tarraingte agus geallann an cód:",
|
||||
"repo.settings.default_branch_desc": "Roghnaigh brainse réamhshocraithe le haghaidh tiomnuithe cóid.",
|
||||
"repo.settings.default_target_branch_desc": "Is féidir le hiarratais tarraingthe brainse sprice réamhshocraithe difriúil a úsáid má tá sé socraithe sa rannán Iarratais Tarraingthe de na Socruithe Ardleibhéil Stórála.",
|
||||
"repo.settings.merge_style_desc": "Stíleanna Cumaisc",
|
||||
"repo.settings.default_merge_style_desc": "Stíl Cumaisc Réamhshocraithe",
|
||||
"repo.settings.default_merge_style_desc": "Stíl chumasc réamhshocraithe",
|
||||
"repo.settings.choose_branch": "Roghnaigh brainse…",
|
||||
"repo.settings.no_protected_branch": "Níl aon bhrainsí cosanta ann.",
|
||||
"repo.settings.edit_protected_branch": "Cuir in eagar",
|
||||
@ -2650,7 +2653,7 @@
|
||||
"repo.branch.restore_success": "Tá brainse \"%s\" curtha ar ais.",
|
||||
"repo.branch.restore_failed": "Theip ar chur ar ais brainse \"%s\".",
|
||||
"repo.branch.protected_deletion_failed": "Tá brainse \"%s\" cosanta. Ní féidir é a scriosadh.",
|
||||
"repo.branch.default_deletion_failed": "Is é brainse \"%s\" an brainse réamhshocraithe. Ní féidir é a scriosadh.",
|
||||
"repo.branch.default_deletion_failed": "Is é brainse \"%s\" an brainse sprioc réamhshocraithe nó an brainse sprioc don iarratas tarraingthe. Ní féidir é a scriosadh.",
|
||||
"repo.branch.default_branch_not_exist": "Níl an brainse réamhshocraithe \"%s\" ann.",
|
||||
"repo.branch.restore": "Athchóirigh Brainse \"%s\"",
|
||||
"repo.branch.download": "Brainse Íosluchtaithe \"%s\"",
|
||||
|
||||
34
package.json
34
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"engines": {
|
||||
"node": ">= 22.6.0",
|
||||
"pnpm": ">= 10.0.0"
|
||||
@ -26,7 +26,7 @@
|
||||
"chart.js": "4.5.1",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"clippie": "4.1.9",
|
||||
"clippie": "4.1.10",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"css-loader": "7.1.3",
|
||||
@ -58,19 +58,19 @@
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.1",
|
||||
"vanilla-colorful": "0.7.2",
|
||||
"vue": "3.5.27",
|
||||
"vue": "3.5.28",
|
||||
"vue-bar-graph": "2.2.0",
|
||||
"vue-chartjs": "5.3.3",
|
||||
"vue-loader": "17.4.2",
|
||||
"webpack": "5.104.1",
|
||||
"webpack": "5.105.0",
|
||||
"webpack-cli": "6.0.1",
|
||||
"wrap-ansi": "9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.6.0",
|
||||
"@eslint/json": "0.14.0",
|
||||
"@playwright/test": "1.58.1",
|
||||
"@stylistic/eslint-plugin": "5.7.1",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@stylistic/eslint-plugin": "5.8.0",
|
||||
"@stylistic/stylelint-plugin": "5.0.1",
|
||||
"@types/codemirror": "5.60.17",
|
||||
"@types/dropzone": "5.7.9",
|
||||
@ -83,9 +83,9 @@
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/toastify-js": "1.12.4",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"@vitest/eslint-plugin": "1.6.6",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@vitejs/plugin-vue": "6.0.4",
|
||||
"@vitest/eslint-plugin": "1.6.7",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-array-func": "5.1.0",
|
||||
@ -97,25 +97,25 @@
|
||||
"eslint-plugin-unicorn": "62.0.0",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.12.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"globals": "17.2.0",
|
||||
"happy-dom": "20.4.0",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"globals": "17.3.0",
|
||||
"happy-dom": "20.6.0",
|
||||
"jiti": "2.6.1",
|
||||
"markdownlint-cli": "0.47.0",
|
||||
"material-icon-theme": "5.31.0",
|
||||
"nolyfill": "1.0.44",
|
||||
"postcss-html": "1.8.1",
|
||||
"spectral-cli-bundle": "1.0.3",
|
||||
"stylelint": "17.1.0",
|
||||
"spectral-cli-bundle": "1.0.4",
|
||||
"stylelint": "17.1.1",
|
||||
"stylelint-config-recommended": "18.0.0",
|
||||
"stylelint-declaration-block-no-ignored-properties": "3.0.0",
|
||||
"stylelint-declaration-strict-value": "1.10.11",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.1.1",
|
||||
"svgo": "4.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"updates": "17.0.9",
|
||||
"vite-string-plugin": "2.0.0",
|
||||
"typescript-eslint": "8.55.0",
|
||||
"updates": "17.4.0",
|
||||
"vite-string-plugin": "2.0.1",
|
||||
"vitest": "4.0.18",
|
||||
"vue-tsc": "3.2.4"
|
||||
},
|
||||
|
||||
1019
pnpm-lock.yaml
generated
1019
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -158,16 +158,16 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||
if link.Href == "#" {
|
||||
link.Href = srcLink
|
||||
}
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionMirrorSyncCreate:
|
||||
srcLink := toSrcLink(ctx, act)
|
||||
if link.Href == "#" {
|
||||
link.Href = srcLink
|
||||
}
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionMirrorSyncDelete:
|
||||
link.Href = act.GetRepoAbsoluteLink(ctx)
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionApprovePullRequest:
|
||||
pullLink := toPullLink(ctx, act)
|
||||
titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
gocontext "context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
@ -744,13 +743,16 @@ func attachHiddenCommentIDs(section *gitdiff.DiffSection, lineComments map[int64
|
||||
// ExcerptBlob render blob excerpt contents
|
||||
func ExcerptBlob(ctx *context.Context) {
|
||||
commitID := ctx.PathParam("sha")
|
||||
lastLeft := ctx.FormInt("last_left")
|
||||
lastRight := ctx.FormInt("last_right")
|
||||
idxLeft := ctx.FormInt("left")
|
||||
idxRight := ctx.FormInt("right")
|
||||
leftHunkSize := ctx.FormInt("left_hunk_size")
|
||||
rightHunkSize := ctx.FormInt("right_hunk_size")
|
||||
direction := ctx.FormString("direction")
|
||||
opts := gitdiff.BlobExcerptOptions{
|
||||
LastLeft: ctx.FormInt("last_left"),
|
||||
LastRight: ctx.FormInt("last_right"),
|
||||
LeftIndex: ctx.FormInt("left"),
|
||||
RightIndex: ctx.FormInt("right"),
|
||||
LeftHunkSize: ctx.FormInt("left_hunk_size"),
|
||||
RightHunkSize: ctx.FormInt("right_hunk_size"),
|
||||
Direction: ctx.FormString("direction"),
|
||||
Language: ctx.FormString("filelang"),
|
||||
}
|
||||
filePath := ctx.FormString("path")
|
||||
gitRepo := ctx.Repo.GitRepo
|
||||
|
||||
@ -770,61 +772,27 @@ func ExcerptBlob(ctx *context.Context) {
|
||||
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
|
||||
}
|
||||
|
||||
chunkSize := gitdiff.BlobExcerptChunkSize
|
||||
commit, err := gitRepo.GetCommit(commitID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetCommit")
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return
|
||||
}
|
||||
section := &gitdiff.DiffSection{
|
||||
FileName: filePath,
|
||||
}
|
||||
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
|
||||
idxLeft -= chunkSize
|
||||
idxRight -= chunkSize
|
||||
leftHunkSize += chunkSize
|
||||
rightHunkSize += chunkSize
|
||||
section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
|
||||
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
|
||||
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
|
||||
lastLeft += chunkSize
|
||||
lastRight += chunkSize
|
||||
} else {
|
||||
offset := -1
|
||||
if direction == "down" {
|
||||
offset = 0
|
||||
}
|
||||
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset)
|
||||
leftHunkSize = 0
|
||||
rightHunkSize = 0
|
||||
idxLeft = lastLeft
|
||||
idxRight = lastRight
|
||||
}
|
||||
blob, err := commit.Tree.GetBlobByPath(filePath)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines")
|
||||
ctx.ServerError("GetBlobByPath", err)
|
||||
return
|
||||
}
|
||||
|
||||
newLineSection := &gitdiff.DiffLine{
|
||||
Type: gitdiff.DiffLineSection,
|
||||
SectionInfo: &gitdiff.DiffLineSectionInfo{
|
||||
Path: filePath,
|
||||
LastLeftIdx: lastLeft,
|
||||
LastRightIdx: lastRight,
|
||||
LeftIdx: idxLeft,
|
||||
RightIdx: idxRight,
|
||||
LeftHunkSize: leftHunkSize,
|
||||
RightHunkSize: rightHunkSize,
|
||||
},
|
||||
reader, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
ctx.ServerError("DataAsync", err)
|
||||
return
|
||||
}
|
||||
if newLineSection.GetExpandDirection() != "" {
|
||||
newLineSection.Content = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
|
||||
switch direction {
|
||||
case "up":
|
||||
section.Lines = append([]*gitdiff.DiffLine{newLineSection}, section.Lines...)
|
||||
case "down":
|
||||
section.Lines = append(section.Lines, newLineSection)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
section, err := gitdiff.BuildBlobExcerptDiffSection(filePath, reader, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("BuildBlobExcerptDiffSection", err)
|
||||
return
|
||||
}
|
||||
|
||||
diffBlobExcerptData.PullIssueIndex = ctx.FormInt64("pull_issue_index")
|
||||
@ -865,37 +833,3 @@ func ExcerptBlob(ctx *context.Context) {
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBlobExcerpt)
|
||||
}
|
||||
|
||||
func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
|
||||
blob, err := commit.Tree.GetBlobByPath(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var diffLines []*gitdiff.DiffLine
|
||||
for line := 0; line < idxRight+chunkSize; line++ {
|
||||
if ok := scanner.Scan(); !ok {
|
||||
break
|
||||
}
|
||||
if line < idxRight {
|
||||
continue
|
||||
}
|
||||
lineText := scanner.Text()
|
||||
diffLine := &gitdiff.DiffLine{
|
||||
LeftIdx: idxLeft + (line - idxRight) + 1,
|
||||
RightIdx: line + 1,
|
||||
Type: gitdiff.DiffLinePlain,
|
||||
Content: " " + lineText,
|
||||
}
|
||||
diffLines = append(diffLines, diffLine)
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("getExcerptLines scan: %w", err)
|
||||
}
|
||||
return diffLines, nil
|
||||
}
|
||||
|
||||
@ -6,12 +6,13 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func DiffPreviewPost(ctx *context.Context) {
|
||||
content := ctx.FormString("content")
|
||||
newContent := ctx.FormString("content")
|
||||
treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||
if treePath == "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
|
||||
@ -27,7 +28,12 @@ func DiffPreviewPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
|
||||
oldContent, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlobContent", err)
|
||||
return
|
||||
}
|
||||
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, oldContent, newContent)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffPreview", err)
|
||||
return
|
||||
|
||||
@ -81,6 +81,8 @@ type DiffLine struct {
|
||||
|
||||
// DiffLineSectionInfo represents diff line section meta data
|
||||
type DiffLineSectionInfo struct {
|
||||
language *diffVarMutable[string]
|
||||
|
||||
Path string
|
||||
|
||||
// These line "idx" are 1-based line numbers
|
||||
@ -165,16 +167,19 @@ func (d *DiffLine) GetLineTypeMarker() string {
|
||||
}
|
||||
|
||||
func (d *DiffLine) getBlobExcerptQuery() string {
|
||||
query := fmt.Sprintf(
|
||||
language := ""
|
||||
if d.SectionInfo.language != nil { // for normal cases, it can't be nil, this check is only for some tests
|
||||
language = d.SectionInfo.language.value
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"last_left=%d&last_right=%d&"+
|
||||
"left=%d&right=%d&"+
|
||||
"left_hunk_size=%d&right_hunk_size=%d&"+
|
||||
"path=%s",
|
||||
"path=%s&filelang=%s",
|
||||
d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx,
|
||||
d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx,
|
||||
d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize,
|
||||
url.QueryEscape(d.SectionInfo.Path))
|
||||
return query
|
||||
url.QueryEscape(d.SectionInfo.Path), url.QueryEscape(language))
|
||||
}
|
||||
|
||||
func (d *DiffLine) GetExpandDirection() string {
|
||||
@ -266,11 +271,12 @@ func FillHiddenCommentIDsForDiffLine(line *DiffLine, lineComments map[int64][]*i
|
||||
line.SectionInfo.HiddenCommentIDs = hiddenCommentIDs
|
||||
}
|
||||
|
||||
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
||||
func newDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
||||
leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line)
|
||||
|
||||
return &DiffLineSectionInfo{
|
||||
Path: treePath,
|
||||
Path: curFile.Name,
|
||||
language: &curFile.language,
|
||||
LastLeftIdx: lastLeftIdx,
|
||||
LastRightIdx: lastRightIdx,
|
||||
LeftIdx: leftLine,
|
||||
@ -290,7 +296,10 @@ func getLineContent(content string, locale translation.Locale) DiffInline {
|
||||
|
||||
// DiffSection represents a section of a DiffFile.
|
||||
type DiffSection struct {
|
||||
file *DiffFile
|
||||
language *diffVarMutable[string]
|
||||
highlightedLeftLines *diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines *diffVarMutable[map[int]template.HTML]
|
||||
|
||||
FileName string
|
||||
Lines []*DiffLine
|
||||
}
|
||||
@ -339,9 +348,9 @@ func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType,
|
||||
var fileLanguage string
|
||||
var highlightedLeftLines, highlightedRightLines map[int]template.HTML
|
||||
// when a "diff section" is manually prepared by ExcerptBlob, it doesn't have "file" information
|
||||
if diffSection.file != nil {
|
||||
fileLanguage = diffSection.file.Language
|
||||
highlightedLeftLines, highlightedRightLines = diffSection.file.highlightedLeftLines, diffSection.file.highlightedRightLines
|
||||
if diffSection.language != nil {
|
||||
fileLanguage = diffSection.language.value
|
||||
highlightedLeftLines, highlightedRightLines = diffSection.highlightedLeftLines.value, diffSection.highlightedRightLines.value
|
||||
}
|
||||
|
||||
var lineHTML template.HTML
|
||||
@ -392,6 +401,11 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
|
||||
}
|
||||
}
|
||||
|
||||
// diffVarMutable is a wrapper to make a variable mutable to be shared across structs
|
||||
type diffVarMutable[T any] struct {
|
||||
value T
|
||||
}
|
||||
|
||||
// DiffFile represents a file diff.
|
||||
type DiffFile struct {
|
||||
// only used internally to parse Ambiguous filenames
|
||||
@ -418,7 +432,6 @@ type DiffFile struct {
|
||||
IsIncompleteLineTooLong bool
|
||||
|
||||
// will be filled by the extra loop in GitDiffForRender
|
||||
Language string
|
||||
IsGenerated bool
|
||||
IsVendored bool
|
||||
SubmoduleDiffInfo *SubmoduleDiffInfo // IsSubmodule==true, then there must be a SubmoduleDiffInfo
|
||||
@ -430,9 +443,10 @@ type DiffFile struct {
|
||||
IsViewed bool // User specific
|
||||
HasChangedSinceLastReview bool // User specific
|
||||
|
||||
// for render purpose only, will be filled by the extra loop in GitDiffForRender
|
||||
highlightedLeftLines map[int]template.HTML
|
||||
highlightedRightLines map[int]template.HTML
|
||||
// for render purpose only, will be filled by the extra loop in GitDiffForRender, the maps of lines are 0-based
|
||||
language diffVarMutable[string]
|
||||
highlightedLeftLines diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines diffVarMutable[map[int]template.HTML]
|
||||
}
|
||||
|
||||
// GetType returns type of diff file.
|
||||
@ -469,6 +483,7 @@ func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommi
|
||||
Type: DiffLineSection,
|
||||
Content: " ",
|
||||
SectionInfo: &DiffLineSectionInfo{
|
||||
language: &diffFile.language,
|
||||
Path: diffFile.Name,
|
||||
LastLeftIdx: lastLine.LeftIdx,
|
||||
LastRightIdx: lastLine.RightIdx,
|
||||
@ -907,6 +922,14 @@ func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
|
||||
return line, err
|
||||
}
|
||||
|
||||
func newDiffSectionForDiffFile(curFile *DiffFile) *DiffSection {
|
||||
return &DiffSection{
|
||||
language: &curFile.language,
|
||||
highlightedLeftLines: &curFile.highlightedLeftLines,
|
||||
highlightedRightLines: &curFile.highlightedRightLines,
|
||||
}
|
||||
}
|
||||
|
||||
func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
|
||||
sb := strings.Builder{}
|
||||
|
||||
@ -964,12 +987,12 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
line := sb.String()
|
||||
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
lastLeftIdx = -1
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
|
||||
// FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug.
|
||||
lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
|
||||
lineSectionInfo := newDiffLineSectionInfo(curFile, line, leftLine-1, rightLine-1)
|
||||
diffLine := &DiffLine{
|
||||
Type: DiffLineSection,
|
||||
Content: line,
|
||||
@ -1004,7 +1027,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
rightLine++
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
lastLeftIdx = -1
|
||||
}
|
||||
@ -1037,7 +1060,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
}
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
lastLeftIdx = -1
|
||||
}
|
||||
@ -1064,7 +1087,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
lastLeftIdx = -1
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
}
|
||||
curSection.Lines = append(curSection.Lines, diffLine)
|
||||
@ -1309,7 +1332,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated()
|
||||
language := attrs.GetLanguage()
|
||||
if language.Has() {
|
||||
diffFile.Language = language.Value()
|
||||
diffFile.language.value = language.Value()
|
||||
}
|
||||
attrDiff = attrs.Get(attribute.Diff).ToString()
|
||||
}
|
||||
@ -1335,11 +1358,11 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
|
||||
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
|
||||
if shouldFullFileHighlight {
|
||||
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
|
||||
if limitedContent.LeftContent != nil {
|
||||
diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
|
||||
}
|
||||
if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
|
||||
if limitedContent.RightContent != nil {
|
||||
diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1347,13 +1370,26 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
func FillDiffFileHighlightLinesByContent(diffFile *DiffFile, left, right []byte) {
|
||||
diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, left)
|
||||
diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, right)
|
||||
}
|
||||
|
||||
func highlightCodeLinesForDiffFile(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
return highlightCodeLines(diffFile.Name, diffFile.language.value, diffFile.Sections, isLeft, rawContent)
|
||||
}
|
||||
|
||||
func highlightCodeLines(name, lang string, sections []*DiffSection, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
if setting.Git.DisableDiffHighlight || len(rawContent) > MaxDiffHighlightEntireFileSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
|
||||
highlightedNewContent, _ := highlight.RenderCodeFast(diffFile.Name, diffFile.Language, content)
|
||||
highlightedNewContent, _ := highlight.RenderCodeFast(name, lang, content)
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
|
||||
lines := make(map[int]template.HTML, len(unsafeLines))
|
||||
// only save the highlighted lines we need, but not the whole file, to save memory
|
||||
for _, sec := range diffFile.Sections {
|
||||
for _, sec := range sections {
|
||||
for _, ln := range sec.Lines {
|
||||
lineIdx := ln.LeftIdx
|
||||
if !isLeft {
|
||||
|
||||
121
services/gitdiff/gitdiff_excerpt.go
Normal file
121
services/gitdiff/gitdiff_excerpt.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitdiff
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type BlobExcerptOptions struct {
|
||||
LastLeft int
|
||||
LastRight int
|
||||
LeftIndex int
|
||||
RightIndex int
|
||||
LeftHunkSize int
|
||||
RightHunkSize int
|
||||
Direction string
|
||||
Language string
|
||||
}
|
||||
|
||||
func fillExcerptLines(section *DiffSection, filePath string, reader io.Reader, lang string, idxLeft, idxRight, chunkSize int) error {
|
||||
buf := &bytes.Buffer{}
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var diffLines []*DiffLine
|
||||
for line := 0; line < idxRight+chunkSize; line++ {
|
||||
if ok := scanner.Scan(); !ok {
|
||||
break
|
||||
}
|
||||
lineText := scanner.Text()
|
||||
if buf.Len()+len(lineText) < int(setting.UI.MaxDisplayFileSize) {
|
||||
buf.WriteString(lineText)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
if line < idxRight {
|
||||
continue
|
||||
}
|
||||
diffLine := &DiffLine{
|
||||
LeftIdx: idxLeft + (line - idxRight) + 1,
|
||||
RightIdx: line + 1,
|
||||
Type: DiffLinePlain,
|
||||
Content: " " + lineText,
|
||||
}
|
||||
diffLines = append(diffLines, diffLine)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("fillExcerptLines scan: %w", err)
|
||||
}
|
||||
section.Lines = diffLines
|
||||
// DiffLinePlain always uses right lines
|
||||
section.highlightedRightLines.value = highlightCodeLines(filePath, lang, []*DiffSection{section}, false /* right */, buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildBlobExcerptDiffSection(filePath string, reader io.Reader, opts BlobExcerptOptions) (*DiffSection, error) {
|
||||
lastLeft, lastRight, idxLeft, idxRight := opts.LastLeft, opts.LastRight, opts.LeftIndex, opts.RightIndex
|
||||
leftHunkSize, rightHunkSize, direction := opts.LeftHunkSize, opts.RightHunkSize, opts.Direction
|
||||
language := opts.Language
|
||||
|
||||
chunkSize := BlobExcerptChunkSize
|
||||
section := &DiffSection{
|
||||
language: &diffVarMutable[string]{value: language},
|
||||
highlightedLeftLines: &diffVarMutable[map[int]template.HTML]{},
|
||||
highlightedRightLines: &diffVarMutable[map[int]template.HTML]{},
|
||||
FileName: filePath,
|
||||
}
|
||||
var err error
|
||||
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
|
||||
idxLeft -= chunkSize
|
||||
idxRight -= chunkSize
|
||||
leftHunkSize += chunkSize
|
||||
rightHunkSize += chunkSize
|
||||
err = fillExcerptLines(section, filePath, reader, language, idxLeft-1, idxRight-1, chunkSize)
|
||||
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
|
||||
err = fillExcerptLines(section, filePath, reader, language, lastLeft, lastRight, chunkSize)
|
||||
lastLeft += chunkSize
|
||||
lastRight += chunkSize
|
||||
} else {
|
||||
offset := -1
|
||||
if direction == "down" {
|
||||
offset = 0
|
||||
}
|
||||
err = fillExcerptLines(section, filePath, reader, language, lastLeft, lastRight, idxRight-lastRight+offset)
|
||||
leftHunkSize = 0
|
||||
rightHunkSize = 0
|
||||
idxLeft = lastLeft
|
||||
idxRight = lastRight
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newLineSection := &DiffLine{
|
||||
Type: DiffLineSection,
|
||||
SectionInfo: &DiffLineSectionInfo{
|
||||
language: &diffVarMutable[string]{value: opts.Language},
|
||||
Path: filePath,
|
||||
LastLeftIdx: lastLeft,
|
||||
LastRightIdx: lastRight,
|
||||
LeftIdx: idxLeft,
|
||||
RightIdx: idxRight,
|
||||
LeftHunkSize: leftHunkSize,
|
||||
RightHunkSize: rightHunkSize,
|
||||
},
|
||||
}
|
||||
if newLineSection.GetExpandDirection() != "" {
|
||||
newLineSection.Content = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
|
||||
switch direction {
|
||||
case "up":
|
||||
section.Lines = append([]*DiffLine{newLineSection}, section.Lines...)
|
||||
case "down":
|
||||
section.Lines = append(section.Lines, newLineSection)
|
||||
}
|
||||
}
|
||||
return section, nil
|
||||
}
|
||||
39
services/gitdiff/gitdiff_excerpt_test.go
Normal file
39
services/gitdiff/gitdiff_excerpt_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitdiff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildBlobExcerptDiffSection(t *testing.T) {
|
||||
data := &bytes.Buffer{}
|
||||
for i := range 100 {
|
||||
data.WriteString("a = " + strconv.Itoa(i+1) + "\n")
|
||||
}
|
||||
|
||||
locale := translation.MockLocale{}
|
||||
lineMiddle := 50
|
||||
diffSection, err := BuildBlobExcerptDiffSection("a.py", bytes.NewReader(data.Bytes()), BlobExcerptOptions{
|
||||
LeftIndex: lineMiddle,
|
||||
RightIndex: lineMiddle,
|
||||
LeftHunkSize: 10,
|
||||
RightHunkSize: 10,
|
||||
Direction: "up",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, diffSection.highlightedRightLines.value, BlobExcerptChunkSize)
|
||||
assert.NotEmpty(t, diffSection.highlightedRightLines.value[lineMiddle-BlobExcerptChunkSize-1])
|
||||
assert.NotEmpty(t, diffSection.highlightedRightLines.value[lineMiddle-2]) // 0-based
|
||||
|
||||
diffInline := diffSection.GetComputedInlineDiffFor(diffSection.Lines[1], locale)
|
||||
assert.Equal(t, `<span class="n">a</span> <span class="o">=</span> <span class="mi">30</span>`+"\n", string(diffInline.Content))
|
||||
}
|
||||
@ -1111,22 +1111,20 @@ func TestDiffLine_GetExpandDirection(t *testing.T) {
|
||||
func TestHighlightCodeLines(t *testing.T) {
|
||||
t.Run("CharsetDetecting", func(t *testing.T) {
|
||||
diffFile := &DiffFile{
|
||||
Name: "a.c",
|
||||
Language: "c",
|
||||
Name: "a.c",
|
||||
Sections: []*DiffSection{
|
||||
{
|
||||
Lines: []*DiffLine{{LeftIdx: 1}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ret := highlightCodeLines(diffFile, true, []byte("// abc\xcc def\xcd")) // ISO-8859-1 bytes
|
||||
ret := highlightCodeLinesForDiffFile(diffFile, true, []byte("// abc\xcc def\xcd")) // ISO-8859-1 bytes
|
||||
assert.Equal(t, "<span class=\"c1\">// abcÌ defÍ\n</span>", string(ret[0]))
|
||||
})
|
||||
|
||||
t.Run("LeftLines", func(t *testing.T) {
|
||||
diffFile := &DiffFile{
|
||||
Name: "a.c",
|
||||
Language: "c",
|
||||
Name: "a.c",
|
||||
Sections: []*DiffSection{
|
||||
{
|
||||
Lines: []*DiffLine{
|
||||
@ -1138,7 +1136,7 @@ func TestHighlightCodeLines(t *testing.T) {
|
||||
},
|
||||
}
|
||||
const nl = "\n"
|
||||
ret := highlightCodeLines(diffFile, true, []byte("a\nb\n"))
|
||||
ret := highlightCodeLinesForDiffFile(diffFile, true, []byte("a\nb\n"))
|
||||
assert.Equal(t, map[int]template.HTML{
|
||||
0: `<span class="n">a</span>` + nl,
|
||||
1: `<span class="n">b</span>`,
|
||||
|
||||
@ -23,7 +23,7 @@ func extractDiffTokenRemainingFullTag(s string) (token, after string, valid bool
|
||||
// keep in mind: even if we'd like to relax this check,
|
||||
// we should never ignore "&" because it is for HTML entity and can't be safely used in the diff algorithm,
|
||||
// because diff between "<" and ">" will generate broken result.
|
||||
isSymbolChar := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || c == '_' || c == '-'
|
||||
isSymbolChar := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || c == '_' || c == '-' || c == '.'
|
||||
if !isSymbolChar {
|
||||
return "", s, false
|
||||
}
|
||||
@ -40,7 +40,7 @@ func extractDiffTokenRemainingFullTag(s string) (token, after string, valid bool
|
||||
|
||||
// Returned token:
|
||||
// * full tag with content: "<<span>content</span>>", it is used to optimize diff results to highlight the whole changed symbol
|
||||
// * opening/close tag: "<span ...>" or "</span>"
|
||||
// * opening/closing tag: "<span ...>" or "</span>"
|
||||
// * HTML entity: "<"
|
||||
func extractDiffToken(s string) (before, token, after string, valid bool) {
|
||||
for pos1 := 0; pos1 < len(s); pos1++ {
|
||||
@ -123,6 +123,25 @@ func (hcd *highlightCodeDiff) collectUsedRunes(code template.HTML) {
|
||||
}
|
||||
}
|
||||
|
||||
func (hcd *highlightCodeDiff) diffEqualPartIsSpaceOnly(s string) bool {
|
||||
for _, r := range s {
|
||||
if r >= hcd.placeholderBegin {
|
||||
recovered := hcd.placeholderTokenMap[r]
|
||||
if strings.HasPrefix(recovered, "<<") {
|
||||
return false // a full tag with content, it can't be space-only
|
||||
} else if strings.HasPrefix(recovered, "<") {
|
||||
continue // a single opening/closing tag, skip the tag and continue to check the content
|
||||
}
|
||||
return false // otherwise, it must be an HTML entity, it can't be space-only
|
||||
}
|
||||
isSpace := r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
if !isSpace {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA, codeB template.HTML) template.HTML {
|
||||
hcd.collectUsedRunes(codeA)
|
||||
hcd.collectUsedRunes(codeB)
|
||||
@ -142,7 +161,21 @@ func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA
|
||||
removedCodePrefix := hcd.registerTokenAsPlaceholder(`<span class="removed-code">`)
|
||||
removedCodeSuffix := hcd.registerTokenAsPlaceholder(`</span><!-- removed-code -->`)
|
||||
|
||||
if removedCodeSuffix != 0 {
|
||||
equalPartSpaceOnly := true
|
||||
for _, diff := range diffs {
|
||||
if diff.Type != diffmatchpatch.DiffEqual {
|
||||
continue
|
||||
}
|
||||
if equalPartSpaceOnly = hcd.diffEqualPartIsSpaceOnly(diff.Text); !equalPartSpaceOnly {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// only add "added"/"removed" tags when needed:
|
||||
// * non-space contents appear in the DiffEqual parts (not a full-line add/del)
|
||||
// * placeholder map still works (not exhausted, can get removedCodeSuffix)
|
||||
addDiffTags := !equalPartSpaceOnly && removedCodeSuffix != 0
|
||||
if addDiffTags {
|
||||
for _, diff := range diffs {
|
||||
switch {
|
||||
case diff.Type == diffmatchpatch.DiffEqual:
|
||||
@ -158,7 +191,7 @@ func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// placeholder map space is exhausted
|
||||
// the caller will still add added/removed backgrounds for the whole line
|
||||
for _, diff := range diffs {
|
||||
take := diff.Type == diffmatchpatch.DiffEqual || (diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd) || (diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel)
|
||||
if take {
|
||||
@ -186,14 +219,7 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlContent template.HTML) s
|
||||
var tagStack []string
|
||||
res := strings.Builder{}
|
||||
|
||||
htmlCode := strings.TrimSpace(string(htmlContent))
|
||||
|
||||
// the standard chroma highlight HTML is `<span class="line [hl]"><span class="cl"> ... </span></span>`
|
||||
// the line wrapper tags should be removed before diff
|
||||
if strings.HasPrefix(htmlCode, `<span class="line`) || strings.HasPrefix(htmlCode, `<span class="cl"`) {
|
||||
htmlCode = strings.TrimSuffix(htmlCode, "</span>")
|
||||
}
|
||||
|
||||
htmlCode := string(htmlContent)
|
||||
var beforeToken, token string
|
||||
var valid bool
|
||||
for {
|
||||
@ -204,10 +230,16 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlContent template.HTML) s
|
||||
// write the content before the token into result string, and consume the token in the string
|
||||
res.WriteString(beforeToken)
|
||||
|
||||
// the standard chroma highlight HTML is `<span class="line [hl]"><span class="cl"> ... </span></span>`
|
||||
// the line wrapper tags should be removed before diff
|
||||
if strings.HasPrefix(token, `<span class="line`) || strings.HasPrefix(token, `<span class="cl"`) {
|
||||
continue
|
||||
}
|
||||
|
||||
var tokenInMap string
|
||||
if strings.HasPrefix(token, "</") { // for closing tag
|
||||
if len(tagStack) == 0 {
|
||||
break // invalid diff result, no opening tag but see closing tag
|
||||
continue // no opening tag but see closing tag, skip it
|
||||
}
|
||||
// make sure the closing tag in map is related to the open tag, to make the diff algorithm can match the opening/closing tags
|
||||
// the closing tag will be recorded in the map by key "</span><!-- <span the-opening> -->" for "<span the-opening>"
|
||||
|
||||
@ -14,25 +14,57 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDiffWithHighlight(t *testing.T) {
|
||||
t.Run("DiffLineAddDel", func(t *testing.T) {
|
||||
func BenchmarkHighlightDiff(b *testing.B) {
|
||||
for b.Loop() {
|
||||
// still fast enough: BenchmarkHighlightDiff-12 1000000 1027 ns/op
|
||||
// TODO: the real bottleneck is that "diffLineWithHighlight" is called twice when rendering "added" and "removed" lines by the caller
|
||||
// Ideally the caller should cache the diff result, and then use the diff result to render "added" and "removed" lines separately
|
||||
hcd := newHighlightCodeDiff()
|
||||
codeA := template.HTML(`x <span class="k">foo</span> y`)
|
||||
codeB := template.HTML(`x <span class="k">bar</span> y`)
|
||||
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, `x <span class="removed-code"><span class="k">foo</span></span> y`, string(outDel))
|
||||
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, `x <span class="added-code"><span class="k">bar</span></span> y`, string(outAdd))
|
||||
hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffWithHighlight(t *testing.T) {
|
||||
t.Run("DiffLineAddDel", func(t *testing.T) {
|
||||
t.Run("WithDiffTags", func(t *testing.T) {
|
||||
hcd := newHighlightCodeDiff()
|
||||
codeA := template.HTML(`x <span class="k">foo</span> y`)
|
||||
codeB := template.HTML(`x <span class="k">bar</span> y`)
|
||||
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, `x <span class="removed-code"><span class="k">foo</span></span> y`, string(outDel))
|
||||
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, `x <span class="added-code"><span class="k">bar</span></span> y`, string(outAdd))
|
||||
})
|
||||
t.Run("NoRedundantTags", func(t *testing.T) {
|
||||
// the equal parts only contain spaces, in this case, don't use "added/removed" tags
|
||||
// because the diff lines already have a background color to indicate the change
|
||||
hcd := newHighlightCodeDiff()
|
||||
codeA := template.HTML("<span> </span> \t<span>foo</span> ")
|
||||
codeB := template.HTML(" <span>bar</span> \n")
|
||||
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, string(codeA), string(outDel))
|
||||
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, string(codeB), string(outAdd))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CleanUp", func(t *testing.T) {
|
||||
hcd := newHighlightCodeDiff()
|
||||
codeA := template.HTML(`<span class="cm">this is a comment</span>`)
|
||||
codeB := template.HTML(`<span class="cm">this is updated comment</span>`)
|
||||
codeA := template.HTML(` <span class="cm">this is a comment</span>`)
|
||||
codeB := template.HTML(` <span class="cm">this is updated comment</span>`)
|
||||
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, `<span class="cm">this is <span class="removed-code">a</span> comment</span>`, string(outDel))
|
||||
assert.Equal(t, ` <span class="cm">this is <span class="removed-code">a</span> comment</span>`, string(outDel))
|
||||
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, `<span class="cm">this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
|
||||
assert.Equal(t, ` <span class="cm">this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
|
||||
|
||||
codeA = `<span class="line"><span>line1</span></span>` + "\n" + `<span class="cl"><span>line2</span></span>`
|
||||
codeB = `<span class="cl"><span>line1</span></span>` + "\n" + `<span class="line"><span>line!</span></span>`
|
||||
outDel = hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, `<span>line1</span>`+"\n"+`<span class="removed-code"><span>line2</span></span>`, string(outDel))
|
||||
outAdd = hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, `<span>line1</span>`+"\n"+`<span class="added-code"><span>line!</span></span>`, string(outAdd))
|
||||
})
|
||||
|
||||
t.Run("OpenCloseTags", func(t *testing.T) {
|
||||
|
||||
@ -345,25 +345,43 @@ func (g *GiteaDownloader) GetReleases(ctx context.Context) ([]*base.Release, err
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func (g *GiteaDownloader) getIssueReactions(index int64) ([]*base.Reaction, error) {
|
||||
var reactions []*base.Reaction
|
||||
func (g *GiteaDownloader) getIssueReactions(ctx context.Context, index int64) ([]*base.Reaction, error) {
|
||||
if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil {
|
||||
log.Info("GiteaDownloader: instance to old, skip getIssueReactions")
|
||||
return reactions, nil
|
||||
}
|
||||
rl, _, err := g.client.GetIssueReactions(g.repoOwner, g.repoName, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, reaction := range rl {
|
||||
reactions = append(reactions, &base.Reaction{
|
||||
UserID: reaction.User.ID,
|
||||
UserName: reaction.User.UserName,
|
||||
Content: reaction.Reaction,
|
||||
})
|
||||
allReactions := make([]*base.Reaction, 0, g.maxPerPage)
|
||||
|
||||
for i := 1; ; i++ {
|
||||
// make sure gitea can shutdown gracefully
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, nil
|
||||
default:
|
||||
}
|
||||
|
||||
reactions, _, err := g.client.ListIssueReactions(g.repoOwner, g.repoName, index, gitea_sdk.ListIssueReactionsOptions{ListOptions: gitea_sdk.ListOptions{
|
||||
PageSize: g.maxPerPage,
|
||||
Page: i,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, reaction := range reactions {
|
||||
allReactions = append(allReactions, &base.Reaction{
|
||||
UserID: reaction.User.ID,
|
||||
UserName: reaction.User.UserName,
|
||||
Content: reaction.Reaction,
|
||||
})
|
||||
}
|
||||
|
||||
if !g.pagination || len(reactions) < g.maxPerPage {
|
||||
break
|
||||
}
|
||||
}
|
||||
return reactions, nil
|
||||
return allReactions, nil
|
||||
}
|
||||
|
||||
func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction, error) {
|
||||
@ -388,7 +406,7 @@ func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction
|
||||
}
|
||||
|
||||
// GetIssues returns issues according start and limit
|
||||
func (g *GiteaDownloader) GetIssues(_ context.Context, page, perPage int) ([]*base.Issue, bool, error) {
|
||||
func (g *GiteaDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) {
|
||||
if perPage > g.maxPerPage {
|
||||
perPage = g.maxPerPage
|
||||
}
|
||||
@ -413,7 +431,7 @@ func (g *GiteaDownloader) GetIssues(_ context.Context, page, perPage int) ([]*ba
|
||||
milestone = issue.Milestone.Title
|
||||
}
|
||||
|
||||
reactions, err := g.getIssueReactions(issue.Index)
|
||||
reactions, err := g.getIssueReactions(ctx, issue.Index)
|
||||
if err != nil {
|
||||
WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err)
|
||||
}
|
||||
@ -497,7 +515,7 @@ func (g *GiteaDownloader) GetComments(ctx context.Context, commentable base.Comm
|
||||
}
|
||||
|
||||
// GetPullRequests returns pull requests according page and perPage
|
||||
func (g *GiteaDownloader) GetPullRequests(_ context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
|
||||
func (g *GiteaDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
|
||||
if perPage > g.maxPerPage {
|
||||
perPage = g.maxPerPage
|
||||
}
|
||||
@ -546,7 +564,7 @@ func (g *GiteaDownloader) GetPullRequests(_ context.Context, page, perPage int)
|
||||
mergeCommitSHA = *pr.MergedCommitID
|
||||
}
|
||||
|
||||
reactions, err := g.getIssueReactions(pr.Index)
|
||||
reactions, err := g.getIssueReactions(ctx, pr.Index)
|
||||
if err != nil {
|
||||
WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err)
|
||||
}
|
||||
|
||||
@ -364,11 +364,12 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba
|
||||
|
||||
// SyncTags syncs releases with tags in the database
|
||||
func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
|
||||
return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
|
||||
_, err := repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *GiteaLocalUploader) SyncBranches(ctx context.Context) error {
|
||||
_, err := repo_module.SyncRepoBranchesWithRepo(ctx, g.repo, g.gitRepo, g.doer.ID)
|
||||
_, _, err := repo_module.SyncRepoBranchesWithRepo(ctx, g.repo, g.gitRepo, g.doer.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -29,9 +29,6 @@ import (
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// gitShortEmptySha Git short empty SHA
|
||||
const gitShortEmptySha = "0000000"
|
||||
|
||||
// UpdateAddress writes new address to Git repository and database
|
||||
func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error {
|
||||
u, err := giturl.ParseGitURL(addr)
|
||||
@ -72,127 +69,6 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
|
||||
return repo_model.UpdateRepositoryColsNoAutoTime(ctx, m.Repo, "original_url")
|
||||
}
|
||||
|
||||
// mirrorSyncResult contains information of a updated reference.
|
||||
// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
|
||||
// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
|
||||
type mirrorSyncResult struct {
|
||||
refName git.RefName
|
||||
oldCommitID string
|
||||
newCommitID string
|
||||
}
|
||||
|
||||
// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
|
||||
// possible output example:
|
||||
/*
|
||||
// * [new tag] v0.1.8 -> v0.1.8
|
||||
// * [new branch] master -> origin/master
|
||||
// * [new ref] refs/pull/2/head -> refs/pull/2/head"
|
||||
// - [deleted] (none) -> origin/test // delete a branch
|
||||
// - [deleted] (none) -> 1 // delete a tag
|
||||
// 957a993..a87ba5f test -> origin/test
|
||||
// + f895a1e...957a993 test -> origin/test (forced update)
|
||||
*/
|
||||
// TODO: return whether it's a force update
|
||||
func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
|
||||
results := make([]*mirrorSyncResult, 0, 3)
|
||||
lines := strings.Split(output, "\n")
|
||||
for i := range lines {
|
||||
// Make sure reference name is presented before continue
|
||||
idx := strings.Index(lines[i], "-> ")
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
refName := strings.TrimSpace(lines[i][idx+3:])
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(lines[i], " * [new tag]"): // new tag
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: git.RefNameFromTag(refName),
|
||||
oldCommitID: gitShortEmptySha,
|
||||
})
|
||||
case strings.HasPrefix(lines[i], " * [new branch]"): // new branch
|
||||
refName = strings.TrimPrefix(refName, remoteName+"/")
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: git.RefNameFromBranch(refName),
|
||||
oldCommitID: gitShortEmptySha,
|
||||
})
|
||||
case strings.HasPrefix(lines[i], " * [new ref]"): // new reference
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: git.RefName(refName),
|
||||
oldCommitID: gitShortEmptySha,
|
||||
})
|
||||
case strings.HasPrefix(lines[i], " - "): // Delete reference
|
||||
isTag := !strings.HasPrefix(refName, remoteName+"/")
|
||||
var refFullName git.RefName
|
||||
if strings.HasPrefix(refName, "refs/") {
|
||||
refFullName = git.RefName(refName)
|
||||
} else if isTag {
|
||||
refFullName = git.RefNameFromTag(refName)
|
||||
} else {
|
||||
refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
|
||||
}
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: refFullName,
|
||||
newCommitID: gitShortEmptySha,
|
||||
})
|
||||
case strings.HasPrefix(lines[i], " + "): // Force update
|
||||
if idx := strings.Index(refName, " "); idx > -1 {
|
||||
refName = refName[:idx]
|
||||
}
|
||||
delimIdx := strings.Index(lines[i][3:], " ")
|
||||
if delimIdx == -1 {
|
||||
log.Error("SHA delimiter not found: %q", lines[i])
|
||||
continue
|
||||
}
|
||||
shas := strings.Split(lines[i][3:delimIdx+3], "...")
|
||||
if len(shas) != 2 {
|
||||
log.Error("Expect two SHAs but not what found: %q", lines[i])
|
||||
continue
|
||||
}
|
||||
var refFullName git.RefName
|
||||
if strings.HasPrefix(refName, "refs/") {
|
||||
refFullName = git.RefName(refName)
|
||||
} else {
|
||||
refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
|
||||
}
|
||||
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: refFullName,
|
||||
oldCommitID: shas[0],
|
||||
newCommitID: shas[1],
|
||||
})
|
||||
case strings.HasPrefix(lines[i], " "): // New commits of a reference
|
||||
delimIdx := strings.Index(lines[i][3:], " ")
|
||||
if delimIdx == -1 {
|
||||
log.Error("SHA delimiter not found: %q", lines[i])
|
||||
continue
|
||||
}
|
||||
shas := strings.Split(lines[i][3:delimIdx+3], "..")
|
||||
if len(shas) != 2 {
|
||||
log.Error("Expect two SHAs but not what found: %q", lines[i])
|
||||
continue
|
||||
}
|
||||
var refFullName git.RefName
|
||||
if strings.HasPrefix(refName, "refs/") {
|
||||
refFullName = git.RefName(refName)
|
||||
} else {
|
||||
refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
|
||||
}
|
||||
|
||||
results = append(results, &mirrorSyncResult{
|
||||
refName: refFullName,
|
||||
oldCommitID: shas[0],
|
||||
newCommitID: shas[1],
|
||||
})
|
||||
|
||||
default:
|
||||
log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, gitRepo gitrepo.Repository, timeout time.Duration) error {
|
||||
cmd := gitcmd.NewCommand("remote", "prune").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout)
|
||||
stdout, _, pruneErr := gitrepo.RunCmdString(ctx, gitRepo, cmd)
|
||||
@ -229,7 +105,7 @@ func checkRecoverableSyncError(stderrMessage string) bool {
|
||||
}
|
||||
|
||||
// runSync returns true if sync finished without error.
|
||||
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
|
||||
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResult, bool) {
|
||||
log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo)
|
||||
|
||||
remoteURL, remoteErr := gitrepo.GitRemoteGetURL(ctx, m.Repo, m.GetRemoteName())
|
||||
@ -250,7 +126,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
|
||||
}
|
||||
|
||||
var err error
|
||||
var fetchOutput string // it is from fetch's stderr
|
||||
fetchStdout, fetchStderr, err := gitrepo.RunCmdString(ctx, m.Repo, cmdFetch())
|
||||
if err != nil {
|
||||
// sanitize the output, since it may contain the remote address, which may contain a password
|
||||
@ -284,8 +159,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
fetchOutput = fetchStderr // the result of "git fetch" is in stderr
|
||||
|
||||
if err := gitrepo.WriteCommitGraph(ctx, m.Repo); err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err)
|
||||
}
|
||||
@ -306,14 +179,17 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
|
||||
}
|
||||
|
||||
log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo)
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil {
|
||||
_, results, err := repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0)
|
||||
if err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err)
|
||||
}
|
||||
|
||||
log.Trace("SyncMirrors [repo: %-v]: syncing releases with tags...", m.Repo)
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil {
|
||||
tagResults, err := repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo)
|
||||
if err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err)
|
||||
}
|
||||
results = append(results, tagResults...)
|
||||
gitRepo.Close()
|
||||
|
||||
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
|
||||
@ -381,7 +257,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
|
||||
}
|
||||
|
||||
m.UpdatedUnix = timeutil.TimeStampNow()
|
||||
return parseRemoteUpdateOutput(fetchOutput, m.GetRemoteName()), true
|
||||
return results, true
|
||||
}
|
||||
|
||||
func getRepoPullMirrorLockKey(repoID int64) string {
|
||||
@ -450,42 +326,42 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
||||
|
||||
for _, result := range results {
|
||||
// Discard GitHub pull requests, i.e. refs/pull/*
|
||||
if result.refName.IsPull() {
|
||||
if result.RefName.IsPull() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create reference
|
||||
if result.oldCommitID == gitShortEmptySha {
|
||||
commitID, err := gitRepo.GetRefCommitID(result.refName.String())
|
||||
if result.OldCommitID == "" {
|
||||
commitID, err := gitRepo.GetRefCommitID(result.RefName.String())
|
||||
if err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.RefName, err)
|
||||
continue
|
||||
}
|
||||
objectFormat := git.ObjectFormatFromName(m.Repo.ObjectFormatName)
|
||||
notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
|
||||
RefFullName: result.refName,
|
||||
RefFullName: result.RefName,
|
||||
OldCommitID: objectFormat.EmptyObjectID().String(),
|
||||
NewCommitID: commitID,
|
||||
}, repo_module.NewPushCommits())
|
||||
notify_service.SyncCreateRef(ctx, m.Repo.MustOwner(ctx), m.Repo, result.refName, commitID)
|
||||
notify_service.SyncCreateRef(ctx, m.Repo.MustOwner(ctx), m.Repo, result.RefName, commitID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Delete reference
|
||||
if result.newCommitID == gitShortEmptySha {
|
||||
notify_service.SyncDeleteRef(ctx, m.Repo.MustOwner(ctx), m.Repo, result.refName)
|
||||
if result.NewCommitID == "" {
|
||||
notify_service.SyncDeleteRef(ctx, m.Repo.MustOwner(ctx), m.Repo, result.RefName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Push commits
|
||||
oldCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.oldCommitID)
|
||||
oldCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.OldCommitID)
|
||||
if err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.oldCommitID, err)
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.OldCommitID, err)
|
||||
continue
|
||||
}
|
||||
newCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.newCommitID)
|
||||
newCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.NewCommitID)
|
||||
if err != nil {
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.newCommitID, err)
|
||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.NewCommitID, err)
|
||||
continue
|
||||
}
|
||||
commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
|
||||
@ -509,7 +385,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
||||
theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
|
||||
|
||||
notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
|
||||
RefFullName: result.refName,
|
||||
RefFullName: result.RefName,
|
||||
OldCommitID: oldCommitID,
|
||||
NewCommitID: newCommitID,
|
||||
}, theCommits)
|
||||
@ -548,7 +424,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, results []*mirrorSyncResult) bool {
|
||||
func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, results []*repo_module.SyncResult) bool {
|
||||
if !m.Repo.IsEmpty {
|
||||
return true
|
||||
}
|
||||
@ -562,11 +438,11 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, re
|
||||
}
|
||||
firstName := ""
|
||||
for _, result := range results {
|
||||
if !result.refName.IsBranch() {
|
||||
if !result.RefName.IsBranch() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := result.refName.BranchName()
|
||||
name := result.RefName.BranchName()
|
||||
if len(firstName) == 0 {
|
||||
firstName = name
|
||||
}
|
||||
|
||||
@ -9,62 +9,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseRemoteUpdateOutput(t *testing.T) {
|
||||
output := `
|
||||
* [new tag] v0.1.8 -> v0.1.8
|
||||
* [new branch] master -> origin/master
|
||||
- [deleted] (none) -> origin/test1
|
||||
- [deleted] (none) -> tag1
|
||||
+ f895a1e...957a993 test2 -> origin/test2 (forced update)
|
||||
957a993..a87ba5f test3 -> origin/test3
|
||||
* [new ref] refs/pull/26595/head -> refs/pull/26595/head
|
||||
* [new ref] refs/pull/26595/merge -> refs/pull/26595/merge
|
||||
e0639e38fb..6db2410489 refs/pull/25873/head -> refs/pull/25873/head
|
||||
+ 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge (forced update)
|
||||
`
|
||||
results := parseRemoteUpdateOutput(output, "origin")
|
||||
assert.Len(t, results, 10)
|
||||
assert.Equal(t, "refs/tags/v0.1.8", results[0].refName.String())
|
||||
assert.Equal(t, gitShortEmptySha, results[0].oldCommitID)
|
||||
assert.Empty(t, results[0].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/heads/master", results[1].refName.String())
|
||||
assert.Equal(t, gitShortEmptySha, results[1].oldCommitID)
|
||||
assert.Empty(t, results[1].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/heads/test1", results[2].refName.String())
|
||||
assert.Empty(t, results[2].oldCommitID)
|
||||
assert.Equal(t, gitShortEmptySha, results[2].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/tags/tag1", results[3].refName.String())
|
||||
assert.Empty(t, results[3].oldCommitID)
|
||||
assert.Equal(t, gitShortEmptySha, results[3].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/heads/test2", results[4].refName.String())
|
||||
assert.Equal(t, "f895a1e", results[4].oldCommitID)
|
||||
assert.Equal(t, "957a993", results[4].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/heads/test3", results[5].refName.String())
|
||||
assert.Equal(t, "957a993", results[5].oldCommitID)
|
||||
assert.Equal(t, "a87ba5f", results[5].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/pull/26595/head", results[6].refName.String())
|
||||
assert.Equal(t, gitShortEmptySha, results[6].oldCommitID)
|
||||
assert.Empty(t, results[6].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/pull/26595/merge", results[7].refName.String())
|
||||
assert.Equal(t, gitShortEmptySha, results[7].oldCommitID)
|
||||
assert.Empty(t, results[7].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/pull/25873/head", results[8].refName.String())
|
||||
assert.Equal(t, "e0639e38fb", results[8].oldCommitID)
|
||||
assert.Equal(t, "6db2410489", results[8].newCommitID)
|
||||
|
||||
assert.Equal(t, "refs/pull/25873/merge", results[9].refName.String())
|
||||
assert.Equal(t, "1c97ebc746", results[9].oldCommitID)
|
||||
assert.Equal(t, "976d27d52f", results[9].newCommitID)
|
||||
}
|
||||
|
||||
func Test_checkRecoverableSyncError(t *testing.T) {
|
||||
cases := []struct {
|
||||
recoverable bool
|
||||
|
||||
@ -147,11 +147,11 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
|
||||
if _, _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, 0); err != nil {
|
||||
return fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
|
||||
}
|
||||
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return fmt.Errorf("SyncReleasesWithTags: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// GetDiffPreview produces and returns diff result of a file which is not yet committed.
|
||||
func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, treePath, content string) (*gitdiff.Diff, error) {
|
||||
func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, treePath, oldContent, newContent string) (*gitdiff.Diff, error) {
|
||||
if branch == "" {
|
||||
branch = repo.DefaultBranch
|
||||
}
|
||||
@ -29,7 +29,7 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr
|
||||
}
|
||||
|
||||
// Add the object to the database
|
||||
objectHash, err := t.HashObjectAndWrite(ctx, strings.NewReader(content))
|
||||
objectHash, err := t.HashObjectAndWrite(ctx, strings.NewReader(newContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -38,5 +38,5 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr
|
||||
if err := t.AddObjectToIndex(ctx, "100644", objectHash, treePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.DiffIndex(ctx)
|
||||
return t.DiffIndex(ctx, oldContent, newContent)
|
||||
}
|
||||
|
||||
@ -27,8 +27,30 @@ func TestGetDiffPreview(t *testing.T) {
|
||||
|
||||
branch := ctx.Repo.Repository.DefaultBranch
|
||||
treePath := "README.md"
|
||||
oldContent := "# repo1\n\nDescription for repo1"
|
||||
content := "# repo1\n\nDescription for repo1\nthis is a new line"
|
||||
|
||||
t.Run("Errors", func(t *testing.T) {
|
||||
t.Run("empty repo", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, &repo_model.Repository{}, branch, treePath, oldContent, content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "repository does not exist [id: 0, uid: 0, owner_name: , name: ]")
|
||||
})
|
||||
|
||||
t.Run("bad branch", func(t *testing.T) {
|
||||
badBranch := "bad_branch"
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, badBranch, treePath, oldContent, content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "branch does not exist [name: "+badBranch+"]")
|
||||
})
|
||||
|
||||
t.Run("empty treePath", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, branch, "", oldContent, content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "path is invalid [path: ]")
|
||||
})
|
||||
})
|
||||
|
||||
expectedDiff := &gitdiff.Diff{
|
||||
Files: []*gitdiff.DiffFile{
|
||||
{
|
||||
@ -112,56 +134,22 @@ func TestGetDiffPreview(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("with given branch", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, branch, treePath, content)
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, branch, treePath, oldContent, content)
|
||||
assert.NoError(t, err)
|
||||
expectedBs, err := json.Marshal(expectedDiff)
|
||||
assert.NoError(t, err)
|
||||
bs, err := json.Marshal(diff)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(expectedBs), string(bs))
|
||||
assert.JSONEq(t, string(expectedBs), string(bs))
|
||||
})
|
||||
|
||||
t.Run("empty branch, same results", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, "", treePath, content)
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, "", treePath, oldContent, content)
|
||||
assert.NoError(t, err)
|
||||
expectedBs, err := json.Marshal(expectedDiff)
|
||||
assert.NoError(t, err)
|
||||
bs, err := json.Marshal(diff)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedBs, bs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDiffPreviewErrors(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/repo1")
|
||||
ctx.SetPathParam("id", "1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
branch := ctx.Repo.Repository.DefaultBranch
|
||||
treePath := "README.md"
|
||||
content := "# repo1\n\nDescription for repo1\nthis is a new line"
|
||||
|
||||
t.Run("empty repo", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, &repo_model.Repository{}, branch, treePath, content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "repository does not exist [id: 0, uid: 0, owner_name: , name: ]")
|
||||
})
|
||||
|
||||
t.Run("bad branch", func(t *testing.T) {
|
||||
badBranch := "bad_branch"
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, badBranch, treePath, content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "branch does not exist [name: "+badBranch+"]")
|
||||
})
|
||||
|
||||
t.Run("empty treePath", func(t *testing.T) {
|
||||
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, branch, "", content)
|
||||
assert.Nil(t, diff)
|
||||
assert.EqualError(t, err, "path is invalid [path: ]")
|
||||
assert.JSONEq(t, string(expectedBs), string(bs))
|
||||
})
|
||||
}
|
||||
|
||||
@ -361,7 +361,7 @@ func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.U
|
||||
}
|
||||
|
||||
// DiffIndex returns a Diff of the current index to the head
|
||||
func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context) (*gitdiff.Diff, error) {
|
||||
func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context, oldContent, newContent string) (*gitdiff.Diff, error) {
|
||||
var diff *gitdiff.Diff
|
||||
cmd := gitcmd.NewCommand("diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
@ -383,6 +383,9 @@ func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context) (*gitdiff.Dif
|
||||
return nil, fmt.Errorf("unable to run diff-index pipeline in temporary repo: %w", err)
|
||||
}
|
||||
|
||||
if len(diff.Files) > 0 {
|
||||
gitdiff.FillDiffFileHighlightLinesByContent(diff.Files[0], util.UnsafeStringToBytes(oldContent), util.UnsafeStringToBytes(newContent))
|
||||
}
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
|
||||
@ -177,10 +177,10 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doer.ID); err != nil {
|
||||
if _, _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doer.ID); err != nil {
|
||||
return nil, fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
|
||||
}
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return nil, fmt.Errorf("Sync releases from git tags failed: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@ -145,7 +145,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
|
||||
if _, _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
|
||||
return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
// otherwise, the releases sync will be done out of this function
|
||||
if !opts.Releases {
|
||||
repo.IsMirror = opts.Mirror
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
log.Error("Failed to synchronize tags to releases for repository: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin">
|
||||
<table class="ui very basic striped table unstackable tw-mb-0">
|
||||
<table class="ui very basic table unstackable tw-mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="username" data-sortt-desc="reverseusername">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.notices.system_notice_list"}} ({{ctx.Locale.Tr "admin.total" .Total}})
|
||||
</h4>
|
||||
<table class="ui attached segment select selectable striped table unstackable g-table-auto-ellipsis">
|
||||
<table class="ui attached segment select selectable table unstackable g-table-auto-ellipsis">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="oldest" data-sortt-desc="newest">ID{{SortArrow "oldest" "newest" $.SortType false}}</th>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{{ctx.Locale.Tr "admin.monitor.queues"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "admin.monitor.queue.name"}}</th>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{{ctx.Locale.Tr "admin.monitor.queue" .Queue.GetName}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table">
|
||||
<table class="ui very basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "admin.monitor.queue.name"}}</th>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
{{template "shared/repo/search" .}}
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table selectable unstackable">
|
||||
<table class="ui very basic table selectable unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="oldest" data-sortt-desc="newest">ID{{SortArrow "oldest" "newest" $.SortType false}}</th>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{{ctx.Locale.Tr "admin.dashboard.statistic"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
{{range $statsKey := .StatsKeys}}
|
||||
<tr>
|
||||
<td width="200">{{$statsKey}}</td>
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped selectable table unstackable">
|
||||
<table class="ui very basic selectable table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="oldest" data-sortt-desc="newest">ID{{SortArrow "oldest" "newest" .SortType false}}</th>
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
dnf config-manager --add-repo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
|
||||
{{- end}}
|
||||
|
||||
# Fedora 41+ (DNF5)
|
||||
{{- range $group := .Groups}}
|
||||
{{- if $group}}{{$group = print "/" $group}}{{end}}
|
||||
dnf config-manager addrepo --from-repofile=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url>
|
||||
{{- end}}
|
||||
|
||||
# {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
|
||||
{{- range $group := .Groups}}
|
||||
{{- if $group}}{{$group = print "/" $group}}{{end}}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.preview.overview" (len .VersionsToRemove)}}</p>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "admin.packages.type"}}</th>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</h4>
|
||||
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped fixed table single line">
|
||||
<table class="ui very basic fixed table single line">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
@ -83,7 +83,7 @@
|
||||
</div>
|
||||
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped fixed table single line">
|
||||
<table class="ui very basic fixed table single line">
|
||||
<tbody>
|
||||
{{range .Branches}}
|
||||
<tr>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div class="ui attached table segment commit-table">
|
||||
<table class="ui very basic striped table unstackable" id="commits-table">
|
||||
<table class="ui very basic table unstackable" id="commits-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th>
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
{{ctx.Locale.Tr "actions.runners.task_list"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<table class="ui very basic striped table unstackable">
|
||||
<table class="ui very basic table unstackable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortt-asc="online" data-sortt-desc="offline">
|
||||
@ -72,7 +72,7 @@
|
||||
<td>{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}}</td>
|
||||
<td><span data-tooltip-content="{{.BelongsToOwnerName}}">{{.BelongsToOwnerType.LocaleString ctx.Locale}}</span></td>
|
||||
<td>
|
||||
<span class="flex-text-inline">{{range .AgentLabels}}<span class="ui label">{{.}}</span>{{end}}</span>
|
||||
<span class="flex-text-inline tw-flex-wrap">{{range .AgentLabels}}<span class="ui label">{{.}}</span>{{end}}</span>
|
||||
</td>
|
||||
<td>{{if .LastOnline}}{{DateUtils.TimeSince .LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</td>
|
||||
<td>
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
{{$index := index .GetIssueInfos 0}}
|
||||
{{ctx.Locale.Tr "action.delete_branch" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
|
||||
{{else if .GetOpType.InActions "mirror_sync_push"}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .RefName (.ShortRepoPath ctx)}}
|
||||
{{else if .GetOpType.InActions "mirror_sync_create"}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .RefName (.ShortRepoPath ctx)}}
|
||||
{{else if .GetOpType.InActions "mirror_sync_delete"}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}}
|
||||
{{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .RefName (.ShortRepoPath ctx)}}
|
||||
{{else if .GetOpType.InActions "approve_pull_request"}}
|
||||
{{$index := index .GetIssueInfos 0}}
|
||||
{{ctx.Locale.Tr "action.approve_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
|
||||
|
||||
@ -148,10 +148,10 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
|
||||
func testEditorDiffPreview(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
|
||||
"content": "Hello, World (Edited)\n",
|
||||
"content": "# repo1 (Edited)",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
|
||||
assert.Contains(t, resp.Body.String(), `<span class="added-code"> (Edited)</span>`)
|
||||
}
|
||||
|
||||
func testEditorPatchFile(t *testing.T) {
|
||||
|
||||
@ -36,6 +36,6 @@ func TestListPullCommits(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req = NewRequest(t, "GET", "/user2/repo1/blob_excerpt/985f0301dba5e7b34be866819cd15ad3d8f508ee?last_left=0&last_right=0&left=2&right=2&left_hunk_size=2&right_hunk_size=2&path=README.md&style=split&direction=up")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `<td class="lines-code lines-code-new"><code class="code-inner"># repo1</code>`)
|
||||
assert.Contains(t, resp.Body.String(), `<td class="lines-code lines-code-new"><code class="code-inner"><span class="gh"># repo1`+"\n"+`</span></code>`)
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ export default {
|
||||
exclude: [
|
||||
'@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
|
||||
'cropperjs', // need to migrate to v2 but v2 is not compatible with v1
|
||||
'eslint', // need to migrate to v10
|
||||
'tailwindcss', // need to migrate
|
||||
'@eslint/json', // needs eslint 10
|
||||
],
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@ -127,11 +127,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -384,14 +384,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
version = "4.67.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
--page-spacing: 16px; /* space between page elements */
|
||||
--page-margin-x: 32px; /* minimum space on left and right side of page */
|
||||
--page-space-bottom: 64px; /* space between last page element and footer */
|
||||
--transition-hover-fade: opacity 0.2s ease; /* fade transition for elements that show on hover */
|
||||
|
||||
/* z-index */
|
||||
--z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
top: 8px;
|
||||
right: 6px;
|
||||
padding: 9px;
|
||||
visibility: hidden;
|
||||
animation: fadeout 0.2s both;
|
||||
visibility: hidden; /* prevent from click events even opacity=0 */
|
||||
opacity: 0;
|
||||
transition: var(--transition-hover-fade);
|
||||
}
|
||||
|
||||
/* adjustments for comment content having only 14px font size */
|
||||
@ -23,8 +24,17 @@
|
||||
background: var(--color-secondary-dark-1) !important;
|
||||
}
|
||||
|
||||
/* all rendered code-block elements are in their container,
|
||||
the manually written code-block elements on "packages" pages don't have the container */
|
||||
.markup .code-block-container:hover .code-copy,
|
||||
.markup .code-block:hover .code-copy {
|
||||
visibility: visible;
|
||||
animation: fadein 0.2s both;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.markup .code-copy {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,15 +82,6 @@ code.language-math.is-loading::after {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
|
||||
@keyframes pulse-1p5 {
|
||||
0% {
|
||||
|
||||
@ -196,11 +196,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ui.striped.table > tr:nth-child(2n),
|
||||
.ui.striped.table > tbody > tr:nth-child(2n) {
|
||||
background: var(--color-light);
|
||||
}
|
||||
|
||||
.ui.table[class*="single line"],
|
||||
.ui.table [class*="single line"] {
|
||||
white-space: nowrap;
|
||||
@ -291,37 +286,10 @@
|
||||
.ui.basic.table > tr > td {
|
||||
background: transparent;
|
||||
}
|
||||
.ui.basic.striped.table > tbody > tr:nth-child(2n) {
|
||||
background: var(--color-light);
|
||||
}
|
||||
.ui.basic.striped.selectable.table > tbody > tr:nth-child(2n):hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.ui[class*="very basic"].table {
|
||||
border: none;
|
||||
}
|
||||
.ui[class*="very basic"].table:not(.striped) > tr > th:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tr > td:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.ui[class*="very basic"].table:not(.striped) > tr > th:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tr > td:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child,
|
||||
.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ui.celled.table > tr > th,
|
||||
.ui.celled.table > thead > tr > th,
|
||||
|
||||
@ -774,10 +774,6 @@ td .commit-summary {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
|
||||
background-color: var(--color-light) !important;
|
||||
}
|
||||
|
||||
.repository .data-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ui.label.commit-sign-badge > * {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ui.label.commit-id-short {
|
||||
font-family: var(--fonts-monospace);
|
||||
height: 24px;
|
||||
|
||||
@ -170,7 +170,9 @@ async function loadMoreFiles(btn: Element): Promise<boolean> {
|
||||
const respFileBoxes = respDoc.querySelector('#diff-file-boxes')!;
|
||||
// the response is a full HTML page, we need to extract the relevant contents:
|
||||
// * append the newly loaded file list items to the existing list
|
||||
document.querySelector('#diff-incomplete')!.replaceWith(...Array.from(respFileBoxes.children));
|
||||
const respFileBoxesChildren = Array.from(respFileBoxes.children); // "children:HTMLCollection" will be empty after replaceWith
|
||||
document.querySelector('#diff-incomplete')!.replaceWith(...respFileBoxesChildren);
|
||||
for (const el of respFileBoxesChildren) window.htmx.process(el);
|
||||
onShowMoreFiles();
|
||||
return true;
|
||||
} catch (error) {
|
||||
@ -200,7 +202,7 @@ function initRepoDiffShowMore() {
|
||||
const resp = await response.text();
|
||||
const respDoc = parseDom(resp, 'text/html');
|
||||
const respFileBody = respDoc.querySelector('#diff-file-boxes .diff-file-body .file-body')!;
|
||||
const respFileBodyChildren = Array.from(respFileBody.children); // respFileBody.children will be empty after replaceWith
|
||||
const respFileBodyChildren = Array.from(respFileBody.children); // "children:HTMLCollection" will be empty after replaceWith
|
||||
el.parentElement!.replaceWith(...respFileBodyChildren);
|
||||
for (const el of respFileBodyChildren) window.htmx.process(el);
|
||||
// FIXME: calling onShowMoreFiles is not quite right here.
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import type {TributeCollection} from 'tributejs';
|
||||
import type {MentionValue} from '../types.ts';
|
||||
|
||||
export async function attachTribute(element: HTMLElement) {
|
||||
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
|
||||
@ -28,7 +29,7 @@ export async function attachTribute(element: HTMLElement) {
|
||||
},
|
||||
};
|
||||
|
||||
const mentionCollection: TributeCollection<Record<string, any>> = {
|
||||
const mentionCollection: TributeCollection<MentionValue> = {
|
||||
values: window.config.mentionValues,
|
||||
requireLeadingSpace: true,
|
||||
menuItemTemplate: (item) => {
|
||||
@ -44,7 +45,10 @@ export async function attachTribute(element: HTMLElement) {
|
||||
};
|
||||
|
||||
const tribute = new Tribute({
|
||||
collection: [emojiCollection as TributeCollection<any>, mentionCollection],
|
||||
collection: [
|
||||
emojiCollection as TributeCollection<any>,
|
||||
mentionCollection as TributeCollection<any>,
|
||||
],
|
||||
noMatchTemplate: () => '',
|
||||
});
|
||||
tribute.attach(element);
|
||||
|
||||
8
web_src/js/globals.d.ts
vendored
8
web_src/js/globals.d.ts
vendored
@ -29,13 +29,7 @@ interface Window {
|
||||
pageData: Record<string, any>,
|
||||
notificationSettings: Record<string, any>,
|
||||
enableTimeTracking: boolean,
|
||||
mentionValues: Array<{
|
||||
key: string,
|
||||
value: string,
|
||||
name: string,
|
||||
fullname: string,
|
||||
avatar: string,
|
||||
}>,
|
||||
mentionValues: Array<import('./types.ts').MentionValue>,
|
||||
mermaidMaxSourceCharacters: number,
|
||||
i18n: Record<string, string>,
|
||||
},
|
||||
|
||||
@ -1,16 +1,56 @@
|
||||
import {isDarkTheme, parseDom} from '../utils.ts';
|
||||
import {makeCodeCopyButton} from './codecopy.ts';
|
||||
import {displayError} from './common.ts';
|
||||
import {createElementFromAttrs, queryElems} from '../utils/dom.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {createElementFromAttrs, createElementFromHTML, getCssRootVariablesText, queryElems} from '../utils/dom.ts';
|
||||
import {html} from '../utils/html.ts';
|
||||
import {load as loadYaml} from 'js-yaml';
|
||||
import type {MermaidConfig} from 'mermaid';
|
||||
|
||||
const {mermaidMaxSourceCharacters} = window.config;
|
||||
|
||||
const iframeCss = `:root {color-scheme: normal}
|
||||
body {margin: 0; padding: 0; overflow: hidden}
|
||||
#mermaid {display: block; margin: 0 auto}`;
|
||||
function getIframeCss(): string {
|
||||
// Inherit some styles (e.g.: root variables) from parent document.
|
||||
// The buttons should use the same styles as `button.code-copy`, and align with it.
|
||||
return `
|
||||
${getCssRootVariablesText()}
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; padding: 0; overflow: hidden; }
|
||||
#mermaid { display: block; margin: 0 auto; }
|
||||
|
||||
.view-controller {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: var(--transition-hover-fade);
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
body:hover .view-controller { visibility: visible; opacity: 1; }
|
||||
@media (hover: none) {
|
||||
.view-controller { visibility: visible; opacity: 1; }
|
||||
}
|
||||
.view-controller button {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
padding: 7.5px 10px;
|
||||
border: 1px solid var(--color-light-border);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-button);
|
||||
color: var(--color-text);
|
||||
user-select: none;
|
||||
}
|
||||
.view-controller button:hover { background: var(--color-secondary); }
|
||||
.view-controller button:active { background: var(--color-secondary-dark-1); }
|
||||
`;
|
||||
}
|
||||
|
||||
function isSourceTooLarge(source: string) {
|
||||
return mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters;
|
||||
@ -77,6 +117,76 @@ async function loadMermaid(needElkRender: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function initMermaidViewController(dragElement: SVGSVGElement) {
|
||||
let inited = false, isDragging = false;
|
||||
let currentScale = 1, initLeft = 0, lastLeft = 0, lastTop = 0, lastPageX = 0, lastPageY = 0;
|
||||
const container = dragElement.parentElement!;
|
||||
|
||||
const resetView = () => {
|
||||
currentScale = 1;
|
||||
lastLeft = initLeft;
|
||||
lastTop = 0;
|
||||
dragElement.style.left = `${lastLeft}px`;
|
||||
dragElement.style.top = `${lastTop}px`;
|
||||
dragElement.style.position = 'absolute';
|
||||
dragElement.style.margin = '0';
|
||||
};
|
||||
|
||||
const initAbsolutePosition = () => {
|
||||
if (inited) return;
|
||||
// if we need to drag or zoom, use absolute position and get the current "left" from the "margin: auto" layout.
|
||||
inited = true;
|
||||
initLeft = container.getBoundingClientRect().width / 2 - dragElement.getBoundingClientRect().width / 2;
|
||||
resetView();
|
||||
};
|
||||
|
||||
for (const el of queryElems(container, '[data-control-action]')) {
|
||||
el.addEventListener('click', () => {
|
||||
initAbsolutePosition();
|
||||
switch (el.getAttribute('data-control-action')) {
|
||||
case 'zoom-in':
|
||||
currentScale *= 1.2;
|
||||
break;
|
||||
case 'zoom-out':
|
||||
currentScale /= 1.2;
|
||||
break;
|
||||
case 'reset':
|
||||
resetView();
|
||||
break;
|
||||
}
|
||||
dragElement.style.transform = `scale(${currentScale})`;
|
||||
});
|
||||
}
|
||||
|
||||
dragElement.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // only left mouse button can drag
|
||||
const target = e.target as Element;
|
||||
if (target.closest('div, p, a, span, button, input')) return; // don't start the drag if the click is on an interactive element (e.g.: link, button) or text element
|
||||
|
||||
initAbsolutePosition();
|
||||
isDragging = true;
|
||||
lastPageX = e.pageX;
|
||||
lastPageY = e.pageY;
|
||||
dragElement.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
dragElement.ownerDocument.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
lastLeft = e.pageX - lastPageX + lastLeft;
|
||||
lastTop = e.pageY - lastPageY + lastTop;
|
||||
dragElement.style.left = `${lastLeft}px`;
|
||||
dragElement.style.top = `${lastTop}px`;
|
||||
lastPageX = e.pageX;
|
||||
lastPageY = e.pageY;
|
||||
});
|
||||
|
||||
dragElement.ownerDocument.addEventListener('mouseup', () => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
dragElement.style.removeProperty('cursor');
|
||||
});
|
||||
}
|
||||
|
||||
let elkLayoutsRegistered = false;
|
||||
|
||||
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
|
||||
@ -107,6 +217,13 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
|
||||
suppressErrorRendering: true,
|
||||
});
|
||||
|
||||
const iframeStyleText = getIframeCss();
|
||||
const applyMermaidIframeHeight = (iframe: HTMLIFrameElement, height: number) => {
|
||||
if (!height) return;
|
||||
// use a min-height to make sure the buttons won't overlap.
|
||||
iframe.style.height = `${Math.max(height, 85)}px`;
|
||||
};
|
||||
|
||||
// mermaid is a globally shared instance, its document also says "Multiple calls to this function will be enqueued to run serially."
|
||||
// so here we just simply render the mermaid blocks one by one, no need to do "Promise.all" concurrently
|
||||
for (const block of mermaidBlocks) {
|
||||
@ -122,27 +239,37 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
|
||||
const svgDoc = parseDom(svgText, 'image/svg+xml');
|
||||
const svgNode = (svgDoc.documentElement as unknown) as SVGSVGElement;
|
||||
|
||||
const viewControllerHtml = html`<div class="view-controller"><button data-control-action="zoom-in">+</button><button data-control-action="reset">reset</button><button data-control-action="zoom-out">-</button></div>`;
|
||||
|
||||
// create an iframe to sandbox the svg with styles, and set correct height by reading svg's viewBox height
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.classList.add('markup-content-iframe', 'is-loading');
|
||||
iframe.srcdoc = html`<html><head><style>${htmlRaw(iframeCss)}</style></head><body></body></html>`;
|
||||
// the styles are not ready, so don't really render anything before the "load" event, to avoid flicker of unstyled content
|
||||
iframe.srcdoc = html`<html><head></head><body></body></html>`;
|
||||
|
||||
// although the "viewBox" is optional, mermaid's output should always have a correct viewBox with width and height
|
||||
const iframeHeightFromViewBox = Math.ceil(svgNode.viewBox?.baseVal?.height ?? 0);
|
||||
if (iframeHeightFromViewBox) iframe.style.height = `${iframeHeightFromViewBox}px`;
|
||||
applyMermaidIframeHeight(iframe, iframeHeightFromViewBox);
|
||||
|
||||
// the iframe will be fully reloaded if its DOM context is changed (e.g.: moved in the DOM tree).
|
||||
// to avoid unnecessary reloading, we should insert the iframe to its final position only once.
|
||||
iframe.addEventListener('load', () => {
|
||||
// same origin, so we can operate "iframe body" and all elements directly
|
||||
// same origin, so we can operate "iframe head/body" and all elements directly
|
||||
const style = document.createElement('style');
|
||||
style.textContent = iframeStyleText;
|
||||
iframe.contentDocument!.head.append(style);
|
||||
|
||||
const iframeBody = iframe.contentDocument!.body;
|
||||
iframeBody.append(svgNode);
|
||||
bindFunctions?.(iframeBody); // follow "mermaid.render" doc, attach event handlers to the svg's container
|
||||
iframeBody.append(createElementFromHTML(viewControllerHtml));
|
||||
|
||||
// according to mermaid, the viewBox height should always exist, here just a fallback for unknown cases.
|
||||
// and keep in mind: clientHeight can be 0 if the element is hidden (display: none).
|
||||
if (!iframeHeightFromViewBox && iframeBody.clientHeight) iframe.style.height = `${iframeBody.clientHeight}px`;
|
||||
if (!iframeHeightFromViewBox) applyMermaidIframeHeight(iframe, iframeBody.clientHeight);
|
||||
iframe.classList.remove('is-loading');
|
||||
|
||||
initMermaidViewController(svgNode);
|
||||
});
|
||||
|
||||
const container = createElementFromAttrs('div', {class: 'mermaid-block'}, iframe, makeCodeCopyButton({'data-clipboard-text': source}));
|
||||
|
||||
@ -31,7 +31,7 @@ export function renderAnsi(line: string): string {
|
||||
|
||||
// handle "\rReading...1%\rReading...5%\rReading...100%",
|
||||
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
|
||||
const lines = [];
|
||||
const lines: Array<string> = [];
|
||||
for (const part of line.split('\r')) {
|
||||
if (part === '') continue;
|
||||
const partHtml = ansi_up.ansi_to_html(part);
|
||||
|
||||
@ -2,6 +2,14 @@ export type IntervalId = ReturnType<typeof setInterval>;
|
||||
|
||||
export type Intent = 'error' | 'warning' | 'info';
|
||||
|
||||
export type MentionValue = {
|
||||
key: string,
|
||||
value: string,
|
||||
name: string,
|
||||
fullname: string,
|
||||
avatar: string,
|
||||
};
|
||||
|
||||
export type RequestData = string | FormData | URLSearchParams | Record<string, any>;
|
||||
|
||||
export type RequestOpts = {
|
||||
|
||||
@ -352,6 +352,22 @@ export function isPlainClick(e: MouseEvent) {
|
||||
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
|
||||
}
|
||||
|
||||
let cssRootVariablesTextCache: string = '';
|
||||
export function getCssRootVariablesText(): string {
|
||||
if (cssRootVariablesTextCache) return cssRootVariablesTextCache;
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
let text = ':root {\n';
|
||||
for (let i = 0; i < style.length; i++) {
|
||||
const name = style.item(i);
|
||||
if (name.startsWith('--')) {
|
||||
text += ` ${name}: ${style.getPropertyValue(name)};\n`;
|
||||
}
|
||||
}
|
||||
text += '}\n';
|
||||
cssRootVariablesTextCache = text;
|
||||
return text;
|
||||
}
|
||||
|
||||
let elemIdCounter = 0;
|
||||
export function generateElemId(prefix: string = ''): string {
|
||||
return `${prefix}${elemIdCounter++}`;
|
||||
|
||||
@ -117,7 +117,6 @@ test('GlobCompiler', async () => {
|
||||
for (const c of golangCases) {
|
||||
const compiled = globCompile(c.pattern, c.separators);
|
||||
const msg = `pattern: ${c.pattern}, input: ${c.input}, separators: ${c.separators || '(none)'}, compiled: ${compiled.regexpPattern}`;
|
||||
// eslint-disable-next-line vitest/valid-expect -- Unlike Jest, Vitest supports a message as the second argument
|
||||
expect(compiled.regexp.test(c.input), msg).toBe(c.matched);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user