diff --git a/main.go b/main.go index f77fcf2..aa7955a 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( ) var ( - Version = "v2.7, by v1.21.0 " + Version = "v2.8.8, by v1.21.0, 20260119 " Tags = "" MakeVersion = "" ) diff --git a/models/actions/secret.go b/models/actions/secret.go new file mode 100644 index 0000000..737da74 --- /dev/null +++ b/models/actions/secret.go @@ -0,0 +1,59 @@ +package actions + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" + "xorm.io/builder" +) + +type FindSecretsOptions struct { + db.ListOptions + OwnerID int64 + RepoID int64 + SecretID int64 + Name string +} + +func (opts *FindSecretsOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.SecretID != 0 { + cond = cond.And(builder.Eq{"id": opts.SecretID}) + } + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } + + return cond +} + +func FindSecrets(ctx context.Context, opts FindSecretsOptions) ([]*secret_model.Secret, error) { + var secrets []*secret_model.Secret + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return secrets, sess. + Where(opts.toConds()). + OrderBy("created_unix desc"). + Find(&secrets) +} + +func CountSecrets(ctx context.Context, opts *FindSecretsOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toConds()).Count(new(secret_model.Secret)) +} + +func DeleteSecret(ctx context.Context, secret *secret_model.Secret) error { + if _, err := db.DeleteByID(ctx, secret.ID, secret); err != nil { + return err + } + return nil +} diff --git a/models/actions/variables.go b/models/actions/variables.go new file mode 100644 index 0000000..ebe340a --- /dev/null +++ b/models/actions/variables.go @@ -0,0 +1,65 @@ +package actions + +import ( + "context" + "strings" + + gitea_actions "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "xorm.io/builder" +) + +type FindVariablesOpts struct { + db.ListOptions + IDs []int64 + RepoID int64 + OwnerID int64 // it will be ignored if RepoID is set + Name string +} + +func (opts FindVariablesOpts) ToConds() builder.Cond { + cond := builder.NewCond() + if len(opts.IDs) > 0 { + if len(opts.IDs) == 1 { + cond = cond.And(builder.Eq{"id": opts.IDs[0]}) + } else { + cond = cond.And(builder.In("id", opts.IDs)) + } + } + + // Since we now support instance-level variables, + // there is no need to check for null values for `owner_id` and `repo_id` + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + if opts.RepoID != 0 { // if RepoID is set + // ignore OwnerID and treat it as 0 + cond = cond.And(builder.Eq{"owner_id": 0}) + } else { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } + return cond +} + +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*gitea_actions.ActionVariable, error) { + var variables []*gitea_actions.ActionVariable + sess := db.GetEngine(ctx) + if opts.PageSize != 0 { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + return variables, sess.Where(opts.ToConds()).OrderBy("created_unix desc").Find(&variables) +} + +func CountVariables(ctx context.Context, opts FindVariablesOpts) (int64, error) { + sess := db.GetEngine(ctx) + return sess.Where(opts.ToConds()).Count(new(gitea_actions.ActionVariable)) +} + +func DeleteVariable(ctx context.Context, variable *gitea_actions.ActionVariable) error { + if _, err := db.DeleteByID(ctx, variable.ID, variable); err != nil { + return err + } + return nil +} diff --git a/modules/convert/convert.go b/modules/convert/convert.go index 873e314..bac8e63 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -23,7 +23,7 @@ func ToTag(repo *repo.Repository, gitRepo *git.Repository, t *git.Tag) (tag *api return &api.Tag{}, err } return &api.Tag{ - Name: t.Name, + Name: strings.TrimPrefix(t.Name, "tags/"), Message: strings.TrimSpace(t.Message), ID: t.ID.String(), Commit: tagCommit, diff --git a/modules/structs/secret.go b/modules/structs/secret.go new file mode 100644 index 0000000..c7b38d1 --- /dev/null +++ b/modules/structs/secret.go @@ -0,0 +1,34 @@ +package structs + +import "time" + +// CreateVariableOption the option when creating variable +// swagger:model +type CreateSecretOption struct { + // Value of the variable to create + // + // required: true + Data string `json:"data" binding:"Required"` +} + +// UpdateVariableOption the option when updating variable +// swagger:model +type UpdateSecretOption struct { + // New name for the variable. If the field is empty, the variable name won't be updated. + Name string `json:"name"` + // Value of the variable to update + // + // required: true + Data string `json:"data" binding:"Required"` + + // Description of the variable to update +} + +// Secret represents a secret +// swagger:model +type Secret struct { + // the secret's name + Name string `json:"name"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` +} diff --git a/modules/structs/variable.go b/modules/structs/variable.go new file mode 100644 index 0000000..034523a --- /dev/null +++ b/modules/structs/variable.go @@ -0,0 +1,36 @@ +package structs + +// CreateVariableOption the option when creating variable +// swagger:model +type CreateVariableOption struct { + // Value of the variable to create + // + // required: true + Value string `json:"value" binding:"Required"` +} + +// UpdateVariableOption the option when updating variable +// swagger:model +type UpdateVariableOption struct { + // New name for the variable. If the field is empty, the variable name won't be updated. + Name string `json:"name"` + // Value of the variable to update + // + // required: true + Value string `json:"value" binding:"Required"` + + // Description of the variable to update +} + +// ActionVariable return value of the query API +// swagger:model +type ActionVariable struct { + // the owner to which the variable belongs + OwnerID int64 `json:"owner_id"` + // the repository to which the variable belongs + RepoID int64 `json:"repo_id"` + // the name of the variable + Name string `json:"name"` + // the value of the variable + Data string `json:"data"` +} diff --git a/modules/util/util.go b/modules/util/util.go new file mode 100644 index 0000000..c301b67 --- /dev/null +++ b/modules/util/util.go @@ -0,0 +1,12 @@ +package util + +import "strings" + +func ReserveLineBreakForTextarea(input string) string { + // Since the content is from a form which is a textarea, the line endings are \r\n. + // It's a standard behavior of HTML. + // But we want to store them as \n like what GitHub does. + // And users are unlikely to really need to keep the \r. + // Other than this, we should respect the original content, even leading or trailing spaces. + return strings.ReplaceAll(input, "\r\n", "\n") +} diff --git a/routers/hat/hat.go b/routers/hat/hat.go index 8ac5d76..91611ba 100644 --- a/routers/hat/hat.go +++ b/routers/hat/hat.go @@ -30,6 +30,7 @@ import ( "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/org" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo/actions" + "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo/explore" "code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/user" "github.com/go-chi/cors" ) @@ -121,10 +122,32 @@ func Routers() *web.Route { m.Group("", func() { m.Get("/version", misc.Version) m.Post("/create_pr_version", bind(gitea_api.PullRequestPayload{}), repo.CreatePrVersion) + m.Post("/create_repo_size_limit_hook", bind(gitea_api.RepositoryPayload{}), repo.CreateRepoSizeLimitHook) + m.Post("/update_hook_arm", bind(gitea_api.RepositoryPayload{}), repo.UpdateHookArm) + m.Post("/update_repo_size_limit_hook", bind(gitea_api.RepositoryPayload{}), repo.UpdateRepoSizeLimitHook) m.Group("/repos", func() { + if setting.Indexer.RepoIndexerEnabled { + m.Get("/explore/code", explore.Code) + } m.Group("/{username}/{reponame}", func() { + m.Get("/code", context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode), repo.Code) m.Combo("").Delete(reqToken(), reqOwner(), repo.Delete) m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqAdmin(), actions.ListActionsSecrets) + m.Combo("/{secretname}"). + Delete(reqToken(), reqAdmin(), actions.DeleteActionsSecret). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateSecretOption{}), actions.CreateActionsSecret). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateSecretOption{}), actions.UpdateActionsSecret) + }) + m.Group("/variables", func() { + m.Get("", reqToken(), reqAdmin(), actions.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqAdmin(), actions.GetVariable). + Delete(reqToken(), reqAdmin(), actions.DeleteVariable). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateVariableOption{}), actions.CreateVariable). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateVariableOption{}), actions.UpdateVariable) + }) m.Get("", context.ReferencesGitRepo(), actions.ListActions) m.Post("/disable", reqAdmin(), actions.DisableWorkflowFile) m.Post("/enable", reqAdmin(), actions.EnableWorkflowFile) @@ -132,6 +155,7 @@ func Routers() *web.Route { m.Post("/runs/{run}/jobs/{job}/rerun", reqRepoWriter(unit_model.TypeActions), actions.Rerun) m.Get("/runs/{run}/jobs/{job}/logs", actions.Logs) m.Post("/runs/{run}/rerun", reqRepoWriter(unit_model.TypeActions), actions.Rerun) + m.Post("/runs/{run}/cancel", reqRepoWriter(unit_model.TypeActions), actions.Cancel) m.Post("/runs", context.ReferencesGitRepo(), reqRepoWriter(unit_model.TypeActions), actions.Run) }, reqRepoReader(unit_model.TypeActions)) m.Post("/transfer", reqOwner(), bind(gitea_api.TransferRepoOption{}), repo.Transfer) @@ -156,6 +180,7 @@ func Routers() *web.Route { }, reqRepoReader(unit_model.TypeCode), context.ReferencesGitRepo(true)) m.Group("/wiki", func() { m.Get("/page_names", repo.ListWikiPageNames) + m.Get("/revisions/{pageName}/{revision}", repo.ShowRevision) }) m.Group("/readme", func() { m.Get("", repo.GetReadmeContents) @@ -184,6 +209,7 @@ func Routers() *web.Route { m.Get("", repo.ListPullRequestVersions) m.Get("/{versionId}/diff", context.ReferencesGitRepo(), repo.GetPullRequestVersionDiff) }) + m.Get("/change_status", repo.ChangePullRequestCloseStatus) }) }, mustAllowPulls, reqRepoReader(unit_model.TypeCode), context.ReferencesGitRepo()) m.Group("/releases", func() { @@ -235,11 +261,29 @@ func Routers() *web.Route { if setting.Service.EnableUserHeatmap { m.Get("/heatmap", user.GetUserHeatmapData) } + m.Get("/code", user.Code) }, context_service.UserAssignmentAPI()) }) m.Post("/orgs", reqToken(), bind(gitea_api.CreateOrgOption{}), org.Create) m.Group("/orgs/{org}", func() { m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(hat_api.EditOrgOption{}), org.Edit) + m.Group("/actions", func() { + m.Group("/secrets", func() { + m.Get("", reqToken(), reqAdmin(), org.ListActionsSecrets) + m.Combo("/{secretname}"). + Delete(reqToken(), reqAdmin(), org.DeleteActionsSecret). + Post(reqToken(), reqAdmin(), bind(hat_api.CreateSecretOption{}), org.CreateActionsSecret). + Put(reqToken(), reqAdmin(), bind(hat_api.UpdateSecretOption{}), org.UpdateActionsSecret) + }) + m.Group("/variables", func() { + m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables) + m.Combo("/{variablename}"). + Get(reqToken(), reqOrgOwnership(), org.GetVariable). + Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable). + Post(reqToken(), reqOrgOwnership(), bind(hat_api.CreateVariableOption{}), org.CreateVariable). + Put(reqToken(), reqOrgOwnership(), bind(hat_api.UpdateVariableOption{}), org.UpdateVariable) + }) + }) }, orgAssignment(true)) m.Group("/teams/{teamid}", func() { diff --git a/routers/hat/org/action.go b/routers/hat/org/action.go new file mode 100644 index 0000000..7e06361 --- /dev/null +++ b/routers/hat/org/action.go @@ -0,0 +1,261 @@ +package org + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" + hat_actions_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/actions" +) + +func DeleteActionsSecret(ctx *context.APIContext) { + if err := hat_actions_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecretByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateSecretOption) + ownerID := ctx.Org.Organization.ID + secretName := ctx.Params("secretname") + + v, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + Name: secretName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("secret name %s already exists", secretName)) + return + } + if _, err := hat_actions_service.CreateSecret(ctx, ownerID, 0, secretName, opt.Data); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + ctx.Status(http.StatusCreated) +} + +func UpdateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateSecretOption) + + _, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("secretname"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("secretname") + } + + _, err = hat_actions_service.UpdateSecret(ctx, ctx.Org.Organization.ID, 0, opt.Name, opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) + +} + +func ListActionsSecrets(ctx *context.APIContext) { + opts := &hat_actions_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + count, err := hat_actions_model.CountSecrets(ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + secrets, err := hat_actions_model.FindSecrets(ctx, *opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +func GetVariable(ctx *context.APIContext) { + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + variable := &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +func DeleteVariable(ctx *context.APIContext) { + if err := hat_actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateVariableOption) + ownerID := ctx.Org.Organization.ID + variableName := ctx.Params("variablename") + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ownerID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := hat_actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +func UpdateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateVariableOption) + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := hat_actions_service.UpdateVariableNameData(ctx, v); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariableNameData", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ListVariables(ctx *context.APIContext) { + opts := hat_actions_model.FindVariablesOpts{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + vars, err := hat_actions_model.FindVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + count, err := hat_actions_model.CountVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + variables := make([]*hat_api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} diff --git a/routers/hat/repo/actions/actions.go b/routers/hat/repo/actions/actions.go index 32bf949..7e56bb1 100644 --- a/routers/hat/repo/actions/actions.go +++ b/routers/hat/repo/actions/actions.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/timeutil" + stdCtx "context" actions_model "code.gitea.io/gitea/models/actions" @@ -21,17 +23,265 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/web/repo" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/convert" hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" + hat_actions_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/actions" jobparser "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" "xorm.io/builder" ) +func DeleteActionsSecret(ctx *context.APIContext) { + if err := hat_actions_service.DeleteSecretByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("secretname")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteSecretByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateSecretOption) + repoID := ctx.Repo.Repository.ID + secretName := ctx.Params("secretname") + + v, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + RepoID: repoID, + Name: secretName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("secret name %s already exists", secretName)) + return + } + if _, err := hat_actions_service.CreateSecret(ctx, 0, repoID, secretName, opt.Data); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + ctx.Status(http.StatusCreated) +} + +func UpdateActionsSecret(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateSecretOption) + + _, err := hat_actions_service.GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("secretname"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("secretname") + } + + _, err = hat_actions_service.UpdateSecret(ctx, 0, ctx.Repo.Repository.ID, opt.Name, opt.Data) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateSecret", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) + +} + +func ListActionsSecrets(ctx *context.APIContext) { + opts := &hat_actions_model.FindSecretsOptions{ + RepoID: ctx.Repo.Repository.ID, + ListOptions: utils.GetListOptions(ctx), + } + + count, err := hat_actions_model.CountSecrets(ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + secrets, err := hat_actions_model.FindSecrets(ctx, *opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +func GetVariable(ctx *context.APIContext) { + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + variable := &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + + ctx.JSON(http.StatusOK, variable) +} + +func DeleteVariable(ctx *context.APIContext) { + if err := hat_actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func CreateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.CreateVariableOption) + repoID := ctx.Repo.Repository.ID + variableName := ctx.Params("variablename") + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + RepoID: repoID, + Name: variableName, + }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.InternalServerError(err) + return + } + if v != nil && v.ID > 0 { + ctx.Error(http.StatusConflict, "", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + return + } + + if _, err := hat_actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "CreateVariable", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusCreated) +} + +func UpdateVariable(ctx *context.APIContext) { + opt := web.GetForm(ctx).(*hat_api.UpdateVariableOption) + + v, err := hat_actions_service.GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + Name: ctx.Params("variablename"), + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetVariable", err) + } else { + ctx.InternalServerError(err) + } + return + } + + if opt.Name == "" { + opt.Name = ctx.Params("variablename") + } + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := hat_actions_service.UpdateVariableNameData(ctx, v); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "UpdateVariableNameData", err) + } else { + ctx.InternalServerError(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ListVariables(ctx *context.APIContext) { + opts := hat_actions_model.FindVariablesOpts{ + RepoID: ctx.Repo.Repository.ID, + ListOptions: utils.GetListOptions(ctx), + } + vars, err := hat_actions_model.FindVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + count, err := hat_actions_model.CountVariables(ctx, opts) + + if err != nil { + ctx.InternalServerError(err) + return + } + + variables := make([]*hat_api.ActionVariable, len(vars)) + for i, v := range vars { + variables[i] = &hat_api.ActionVariable{ + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, variables) +} + func Run(ctx *context.APIContext) { workflow := ctx.FormString("workflow") ref := ctx.FormString("ref") @@ -131,6 +381,11 @@ type Workflow struct { ErrMsg string } +type DetailStatus struct { + RunID int64 + Status []actions_model.Status +} + type ResponseAction struct { Workflows []Workflow CurWorkflow string @@ -143,6 +398,7 @@ type ResponseAction struct { Runs actions_model.RunList Actors []*user.User StatusInfoList []actions_model.StatusInfo + DetailStatusList []DetailStatus } func ListActions(ctx *context.APIContext) { @@ -261,7 +517,20 @@ func ListActions(ctx *context.APIContext) { } for _, run := range runs { + var detailStatuses []actions_model.Status + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "actions_model.GetRunJobsByRunID", err.Error()) + return + } + for _, j := range jobs { + detailStatuses = append(detailStatuses, j.Status) + } run.Repo = ctx.Repo.Repository + responseAction.DetailStatusList = append(responseAction.DetailStatusList, DetailStatus{ + RunID: run.ID, + Status: detailStatuses, + }) } if err := runs.LoadTriggerUser(ctx); err != nil { @@ -684,3 +953,44 @@ func disableOrEnableWorkflowFile(ctx *context.APIContext, isEnable bool) { ctx.Status(http.StatusNoContent) } + +func Cancel(ctx *context.APIContext) { + runIndex := ctx.ParamsInt64("run") + + _, jobs := getRunJobs(ctx, runIndex, -1) + if ctx.Written() { + return + } + + if err := db.WithTx(ctx, func(ctx stdCtx.Context) error { + for _, job := range jobs { + status := job.Status + if status.IsDone() { + continue + } + if job.TaskID == 0 { + job.Status = actions_model.StatusCancelled + job.Stopped = timeutil.TimeStampNow() + n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") + if err != nil { + return err + } + if n == 0 { + return fmt.Errorf("job has changed, try again") + } + continue + } + if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil { + return err + } + } + return nil + }); err != nil { + ctx.Error(http.StatusInternalServerError, "runs Cancel", err.Error()) + return + } + + actions_service.CreateCommitStatus(ctx, jobs...) + + ctx.JSON(http.StatusOK, struct{}{}) +} diff --git a/routers/hat/repo/explore/code.go b/routers/hat/repo/explore/code.go new file mode 100644 index 0000000..b3f3089 --- /dev/null +++ b/routers/hat/repo/explore/code.go @@ -0,0 +1,119 @@ +package explore + +import ( + "fmt" + "math" + "net/http" + "strconv" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/context" + code_indexer "code.gitea.io/gitea/modules/indexer/code" + "code.gitea.io/gitea/modules/setting" +) + +func Code(ctx *context.APIContext) { + language := ctx.FormString("language") + keyword := ctx.FormString("keyword") + queryType := ctx.FormString("query_type") + isMatch := queryType == "match" + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + var ( + repoIDs []int64 + err error + isAdmin bool + ) + + if ctx.Doer != nil { + isAdmin = ctx.Doer.IsAdmin + } + + if ctx.Doer == nil || !isAdmin { + repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindUserCodeAccessibleRepoIDs", err) + return + } + } + + var ( + total int + searchResults []*code_indexer.Result + searchResultLanguages []*code_indexer.SearchResultLanguages + ) + + var codeIndexerUnavailable bool + var repoMaps map[int64]*repo_model.Repository + if (len(repoIDs) > 0) || isAdmin { + total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + if err != nil { + if code_indexer.IsAvailable(ctx) { + ctx.Error(http.StatusInternalServerError, "SearchResults", err) + return + } + codeIndexerUnavailable = true + } else { + codeIndexerUnavailable = !code_indexer.IsAvailable(ctx) + } + + loadRepoIDs := make([]int64, 0, len(searchResults)) + for _, result := range searchResults { + var find bool + for _, id := range loadRepoIDs { + if id == result.RepoID { + find = true + break + } + } + if !find { + loadRepoIDs = append(loadRepoIDs, result.RepoID) + } + } + + repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetRepositoriesMapByIDs", err) + return + } + + repoMaps = repoMaps + + if len(loadRepoIDs) != len(repoMaps) { + // Remove deleted repos from search results + cleanedSearchResults := make([]*code_indexer.Result, 0, len(repoMaps)) + for _, sr := range searchResults { + if _, found := repoMaps[sr.RepoID]; found { + cleanedSearchResults = append(cleanedSearchResults, sr) + } + } + + searchResults = cleanedSearchResults + } + } + + pageCount := int(math.Ceil(float64(total) / float64(setting.UI.RepoSearchPagingNum))) + ctx.Resp.Header().Set("X-Page", strconv.Itoa(page)) + ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(setting.UI.RepoSearchPagingNum)) + ctx.Resp.Header().Set("X-Total", strconv.FormatInt(int64(total), 10)) + ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount)) + ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount)) + + ctx.SetLinkHeader(int(total), setting.UI.RepoSearchPagingNum) + ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", total)) + ctx.JSON(http.StatusOK, struct { + CodeIndexerUnavailable bool + RepoMaps map[int64]*repo_model.Repository + SearchResults []*code_indexer.Result + SearchResultLanguages []*code_indexer.SearchResultLanguages + }{ + CodeIndexerUnavailable: codeIndexerUnavailable, + RepoMaps: repoMaps, + SearchResults: searchResults, + SearchResultLanguages: searchResultLanguages, + }) +} diff --git a/routers/hat/repo/pull.go b/routers/hat/repo/pull.go index b02d826..315112c 100644 --- a/routers/hat/repo/pull.go +++ b/routers/hat/repo/pull.go @@ -35,6 +35,41 @@ import ( hat_pull_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/pull" ) +func ChangePullRequestCloseStatus(ctx *context.APIContext) { + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if issues_model.IsErrPullRequestNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + } + return + } + changeStatus := ctx.FormBool("is_closed") + _, err = issues_model.ChangeIssueStatus(ctx, pr.Issue, ctx.Doer, changeStatus) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ChangeIssueStatus", err) + } + + if err = pull_service.TestPatch(pr); err != nil { + ctx.Error(http.StatusInternalServerError, "TestPatch", err) + } + + // Update Commit Divergence + divergence, err := pull_service.GetDiverging(ctx, pr) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetDiverging", err) + } + pr.CommitsAhead = divergence.Ahead + pr.CommitsBehind = divergence.Behind + + if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateColsIfNotMerged", err) + } + + ctx.Status(http.StatusNoContent) +} + func CreatePrVersion(ctx *context.APIContext) { form := web.GetForm(ctx).(*gitea_api.PullRequestPayload) hat_pull_service.AddToTaskQueue(&issues_model.PullRequest{ID: form.PullRequest.ID}, string(form.Action)) diff --git a/routers/hat/repo/search.go b/routers/hat/repo/search.go new file mode 100644 index 0000000..c2bb846 --- /dev/null +++ b/routers/hat/repo/search.go @@ -0,0 +1,68 @@ +package repo + +import ( + "fmt" + "math" + "net/http" + "strconv" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/context" + code_indexer "code.gitea.io/gitea/modules/indexer/code" + + "code.gitea.io/gitea/modules/setting" +) + +func Code(ctx *context.APIContext) { + language := ctx.FormString("language") + keyword := ctx.FormString("keyword") + queryType := ctx.FormString("query_type") + isMatch := queryType == "match" + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + var ( + total int + searchResults []*code_indexer.Result + searchResultLanguages []*code_indexer.SearchResultLanguages + ) + + var codeIndexerUnavailable bool + var repoMaps map[int64]*repo_model.Repository + + total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, + language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + if err != nil { + if code_indexer.IsAvailable(ctx) { + ctx.Error(http.StatusInternalServerError, "SearchResults", err) + return + } + codeIndexerUnavailable = true + } else { + codeIndexerUnavailable = !code_indexer.IsAvailable(ctx) + } + + pageCount := int(math.Ceil(float64(total) / float64(setting.UI.RepoSearchPagingNum))) + ctx.Resp.Header().Set("X-Page", strconv.Itoa(page)) + ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(setting.UI.RepoSearchPagingNum)) + ctx.Resp.Header().Set("X-Total", strconv.FormatInt(int64(total), 10)) + ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount)) + ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount)) + + ctx.SetLinkHeader(int(total), setting.UI.RepoSearchPagingNum) + ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", total)) + ctx.JSON(http.StatusOK, struct { + CodeIndexerUnavailable bool + RepoMaps map[int64]*repo_model.Repository + SearchResults []*code_indexer.Result + SearchResultLanguages []*code_indexer.SearchResultLanguages + }{ + CodeIndexerUnavailable: codeIndexerUnavailable, + RepoMaps: repoMaps, + SearchResults: searchResults, + SearchResultLanguages: searchResultLanguages, + }) +} diff --git a/routers/hat/repo/size_limit.go b/routers/hat/repo/size_limit.go new file mode 100644 index 0000000..235a6f1 --- /dev/null +++ b/routers/hat/repo/size_limit.go @@ -0,0 +1,385 @@ +package repo + +import ( + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + gitea_api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "fmt" + "github.com/unknwon/com" + "io/ioutil" + "net/http" + "os" + "path/filepath" +) + +const ( + SIZE_LIMIT_SCRIPT_NAME = "size_limit" +) + +type UpdateRepoSizeLimit struct { + OwnerName string `json:"owner_name"` + RepoName string `json:"repo_name"` + LimitSize int `json:"limit_size"` +} + +func CreateRepoSizeLimitHook(ctx *context.APIContext) { + form := web.GetForm(ctx).(*gitea_api.RepositoryPayload) + limitMb := ctx.FormInt("limit_size") + if limitMb < 30 { + limitMb = 512 + } + eventName := string(form.Action) + if eventName == "deleted" { + ctx.Status(http.StatusOK) + return + } + getAssignmentRepo(ctx, form.Repository.ID) + hookDir := filepath.Join(ctx.Repo.Repository.RepoPath(), "hooks") + hookName := "pre-receive" + //results := make([]string, 0, 10) + sizeLimitTpls := []string{ + fmt.Sprintf("#!/usr/bin/env %s\n\n\nset -o pipefail\n\nreadonly DEFAULT_FILE_MAXSIZE_MB=\"30\" \nreadonly CONFIG_NAME=\"hooks.maxfilesize\"\nreadonly NULLSHA=\"0000000000000000000000000000000000000000\"\nreadonly EXIT_SUCCESS=0\nreadonly EXIT_FAILURE=1\nreadonly DEFAULT_REPO_MAXSIZE_MB=\"1024\" \nreadonly CHECK_FLAG_ON=1\n\n\nPUSH_SIZE_CHECK_FLAG=1\nREPO_MAX_FILE_SIZE=1024\nREPO_MAX_SIZE=%d\nREPO_CURRENT_SIZE=$(du -sk \"${PWD}\" | cut -f1)\nsize_in_mb=$(($REPO_CURRENT_SIZE / 1024))\nstatus=\"$EXIT_SUCCESS\"\n\n# skip this hook entirely if shell check is not open\ncheck_flag=${PUSH_SIZE_CHECK_FLAG}\nif [[ $check_flag != $CHECK_FLAG_ON ]]; then\nexit $EXIT_SUCCESS\nfi\n\n\n#######################################\n# check the file max size limit\n#######################################\n\n# get maximum filesize (from repository-specific config)\nmaxsize_mb=\"${REPO_MAX_FILE_SIZE}\"\n\nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\necho \"failed to get ${CONFIG_NAME} from config\"\nexit \"$EXIT_FAILURE\"\nfi\n\npush_size=\"0\"\n# read lines from stdin (format: \" \\n\")\nwhile read oldref newref refname; do\n# skip branch deletions\nif [[ \"$newref\" == \"$NULLSHA\" ]]; then\n continue\nfi\n\n# find large objects\n# check all objects from $oldref (possible $NULLSHA) to $newref, but\n# skip all objects that have already been accepted (i.e. are referenced by\n# another branch or tag).\n\nnew_branch_flag=0\nif [[ \"$oldref\" == \"$NULLSHA\" ]]; then\n target=\"$newref\"\n new_branch_flag=1\n echo \"You are creating a new remote branch,openI will check all files in commit history to find oversize files\"\nelse\n target=\"${oldref}..${newref}\"\nfi\nmaxsize=`expr $maxsize_mb \\* 1048576` \n\n# find objects in this push_size\n# print like:\n# 08da8e2ab9ae4095bf94dd71ac913132b880b463 commit 214\n# 43e993b768ede5740e8c65de2ed6edec25053ea1 tree 185\n# 4476971d76569039df7569af1b8d03c288f6b193 blob 20167318 b0417e6593a1.zip\nfiles=\"$(git rev-list --objects \"$target\" | \\\n git cat-file $'--batch-check=%%(objectname) %%(objecttype) %%(objectsize) %%(rest)' | \\\n awk -F ' ' -v maxbytes=\"$maxsize\" 'BEGIN {totalIn=0} {if( $3 > maxbytes && $2 == \"blob\") { totalIn+=$3; print $4} else { totalIn+=$3}} END { printf (\"totalIn=\\t%%s\",totalIn)}' )\"\n \nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\n echo \"failed to check for large files in ref ${refname}\"\n continue\nfi\n\nIFS=$'\\n'\n# rewrite IFS to seperate line in $files\nfor file in $files; do\n # if don't unset IFS,temp_array=(${file}) will get error answer\n \n if [[ ${file} == totalIn=* ]]; then\n\tIFS=$'\\t'\n\ttemp_array=(${file})\n\tpush_size=${temp_array[1]}\n\tcontinue\n fi\n\tunset IFS\n if [[ \"$status\" == $EXIT_SUCCESS ]]; then\n\t\techo -e \"Error: Your push was rejected because it contains files larger than $(numfmt --to=iec \"$maxsize_mb\") Mb\"\n\t\techo \"help document -- ...\"\n\t\techo \"oversize files:\"\n\t\tstatus=\"$EXIT_FAILURE\"\t\n fi\n echo -e \"\\033[31m- ${file}\\033[0m \"\ndone\n\nif [[ \"$status\" != $EXIT_SUCCESS ]]; then\n\texit \"$status\"\nfi\n\ndone\n\n#######################################\n# check the repo max size limit\n#######################################\nif [[ $push_size -eq \"0\" ]]; then\n\texit $EXIT_SUCCESS\nfi\n\n# if create new branch or tag,use count-objects -v to get pack size\nif [[ $new_branch_flag -eq 1 ]]; then\n size_kb=`git count-objects -v | grep 'size-pack' | sed 's/.*\\(size-pack:\\).//'`\n size_pack_kb=`git count-objects -v | grep 'size:' | sed 's/.*\\(size:\\).//'`\n\ttotal_kb=`expr $size_kb + $size_pack_kb`\n\tlet push_size=$total_kb*1024\nfi\n\nsizelimit_mb=\"${REPO_MAX_SIZE}\"\nlet sizelimit_b=$sizelimit_mb*1024*1024\n\n# repo size at here means the size of repo directory in server \nreposize_b=${REPO_CURRENT_SIZE}\n\nlet total=$REPO_CURRENT_SIZE*1024\n\nif [ $total -gt $sizelimit_b ]; then\n echo \"Error: Your push was rejected because the repository size is large than $sizelimit_mb Mb\"\n echo \"Error: 推送被拒绝,文件或目录大小大于 $sizelimit_mb Mb\"\n exit $EXIT_FAILURE\nfi\n\n\nexit $EXIT_SUCCESS", setting.ScriptType, limitMb), + fmt.Sprintf(""), + fmt.Sprintf(""), + } + + oldHookPath := filepath.Join(hookDir, hookName+".d", SIZE_LIMIT_SCRIPT_NAME) + if !com.IsExist(oldHookPath) { + //if err := checkHookFile(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0], results); err != nil { + // ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + // ctx.NotFound() + // return + //} + + if err := writeHookTpl(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0]); err != nil { + ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + ctx.NotFound() + return + } + } + ctx.Status(http.StatusOK) +} + +// 更新arm架构中事件失效问题 +func UpdateHookArm(ctx *context.APIContext) { + form := web.GetForm(ctx).(*gitea_api.RepositoryPayload) + eventName := string(form.Action) + if eventName == "deleted" { + ctx.Status(http.StatusOK) + return + } + getAssignmentRepo(ctx, form.Repository.ID) + if err := UpdateDelegateHooks(ctx.Repo.Repository.RepoPath()); err != nil { + fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", ctx.Repo, err) + ctx.Error(http.StatusInternalServerError, "", "Unable to recreate delegate hooks.") + return + } + ctx.Status(http.StatusOK) +} + +// 更新仓库限制大小 +func UpdateRepoSizeLimitHook(ctx *context.APIContext) { + form := web.GetForm(ctx).(*UpdateRepoSizeLimit) + ownerName := string(form.OwnerName) + repoName := string(form.RepoName) + LimitSize := form.LimitSize + + getAssignmentRepoByName(ctx, ownerName, repoName) + + hookDir := filepath.Join(ctx.Repo.Repository.RepoPath(), "hooks") + hookName := "pre-receive" + //results := make([]string, 0, 10) + sizeLimitTpls := []string{ + //fmt.Sprintf("#!/usr/bin/env %s\n\n\nset -o pipefail\n\nreadonly DEFAULT_FILE_MAXSIZE_MB=\"30\" \nreadonly CONFIG_NAME=\"hooks.maxfilesize\"\nreadonly NULLSHA=\"0000000000000000000000000000000000000000\"\nreadonly EXIT_SUCCESS=0\nreadonly EXIT_FAILURE=1\nreadonly DEFAULT_REPO_MAXSIZE_MB=\"1024\" \nreadonly CHECK_FLAG_ON=1\n\n\nstatus=\"$EXIT_SUCCESS\"\n\n# skip this hook entirely if shell check is not open\ncheck_flag=${PUSH_SIZE_CHECK_FLAG}\nif [[ $check_flag != $CHECK_FLAG_ON ]]; then\nexit $EXIT_SUCCESS\nfi\n\n\n#######################################\n# check the file max size limit\n#######################################\n\n# get maximum filesize (from repository-specific config)\nmaxsize_mb=\"${REPO_MAX_FILE_SIZE}\"\n\nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\necho \"failed to get ${CONFIG_NAME} from config\"\nexit \"$EXIT_FAILURE\"\nfi\n\npush_size=\"0\"\n# read lines from stdin (format: \" \\n\")\nwhile read oldref newref refname; do\n# skip branch deletions\nif [[ \"$newref\" == \"$NULLSHA\" ]]; then\n continue\nfi\n\n# find large objects\n# check all objects from $oldref (possible $NULLSHA) to $newref, but\n# skip all objects that have already been accepted (i.e. are referenced by\n# another branch or tag).\n\nnew_branch_flag=0\nif [[ \"$oldref\" == \"$NULLSHA\" ]]; then\n target=\"$newref\"\n new_branch_flag=1\n echo \"You are creating a new remote branch,openI will check all files in commit history to find oversize files\"\nelse\n target=\"${oldref}..${newref}\"\nfi\nmaxsize=`expr $maxsize_mb \\* 1048576` \n\n# find objects in this push_size\n# print like:\n# 08da8e2ab9ae4095bf94dd71ac913132b880b463 commit 214\n# 43e993b768ede5740e8c65de2ed6edec25053ea1 tree 185\n# 4476971d76569039df7569af1b8d03c288f6b193 blob 20167318 b0417e6593a1.zip\nfiles=\"$(git rev-list --objects \"$target\" | \\\n git cat-file $'--batch-check=%%(objectname) %%(objecttype) %%(objectsize) %%(rest)' | \\\n awk -F ' ' -v maxbytes=\"$maxsize\" 'BEGIN {totalIn=0} {if( $3 > maxbytes && $2 == \"blob\") { totalIn+=$3; print $4} else { totalIn+=$3}} END { printf (\"totalIn=\\t%%s\",totalIn)}' )\"\n \nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\n echo \"failed to check for large files in ref ${refname}\"\n continue\nfi\n\nIFS=$'\\n'\n# rewrite IFS to seperate line in $files\nfor file in $files; do\n # if don't unset IFS,temp_array=(${file}) will get error answer\n \n if [[ ${file} == totalIn=* ]]; then\n\tIFS=$'\\t'\n\ttemp_array=(${file})\n\tpush_size=${temp_array[1]}\n\tcontinue\n fi\n\tunset IFS\n if [[ \"$status\" == $EXIT_SUCCESS ]]; then\n\t\techo -e \"Error: Your push was rejected because it contains files larger than $(numfmt --to=iec \"$maxsize_mb\") Mb\"\n\t\techo \"help document -- https://openi.pcl.ac.cn/zeizei/OpenI_Learning/src/branch/master/docs/git/repository_capacity_help.md\"\n\t\techo \"oversize files:\"\n\t\tstatus=\"$EXIT_FAILURE\"\t\n fi\n echo -e \"\\033[31m- ${file}\\033[0m \"\ndone\n\nif [[ \"$status\" != $EXIT_SUCCESS ]]; then\n\texit \"$status\"\nfi\n\ndone\n\n#######################################\n# check the repo max size limit\n#######################################\nif [[ $push_size -eq \"0\" ]]; then\n\texit $EXIT_SUCCESS\nfi\n\n# if create new branch or tag,use count-objects -v to get pack size\nif [[ $new_branch_flag -eq 1 ]]; then\n size_kb=`git count-objects -v | grep 'size-pack' | sed 's/.*\\(size-pack:\\).//'`\n size_pack_kb=`git count-objects -v | grep 'size:' | sed 's/.*\\(size:\\).//'`\n\ttotal_kb=`expr $size_kb + $size_pack_kb`\n\tlet push_size=$total_kb*1024\nfi\n\nsizelimit_mb=\"${REPO_MAX_SIZE}\"\nlet sizelimit_b=$sizelimit_mb*1024*1024\n\n# repo size at here means the size of repo directory in server \nreposize_b=${REPO_CURRENT_SIZE}\n\ntotal=`expr $push_size + $reposize_b`\n\nif [ $total -gt $sizelimit_b ]; then\n echo \"Error: Your push was rejected because the repository size is large than $sizelimit_mb Mb\"\n echo \"see the help document--https://openi.pcl.ac.cn/zeizei/OpenI_Learning/src/branch/master/docs/git/repository_capacity_help.md\"\n exit $EXIT_FAILURE\nfi\n\n\nexit $EXIT_SUCCESS", setting.ScriptType, LimitSize), + fmt.Sprintf("#!/usr/bin/env %s\n\n\nset -o pipefail\n\nreadonly DEFAULT_FILE_MAXSIZE_MB=\"30\" \nreadonly CONFIG_NAME=\"hooks.maxfilesize\"\nreadonly NULLSHA=\"0000000000000000000000000000000000000000\"\nreadonly EXIT_SUCCESS=0\nreadonly EXIT_FAILURE=1\nreadonly DEFAULT_REPO_MAXSIZE_MB=\"1024\" \nreadonly CHECK_FLAG_ON=1\n\n\nPUSH_SIZE_CHECK_FLAG=1\nREPO_MAX_FILE_SIZE=1024\nREPO_MAX_SIZE=%d\nREPO_CURRENT_SIZE=$(du -sk \"${PWD}\" | cut -f1)\nsize_in_mb=$(($REPO_CURRENT_SIZE / 1024))\nstatus=\"$EXIT_SUCCESS\"\n\n# skip this hook entirely if shell check is not open\ncheck_flag=${PUSH_SIZE_CHECK_FLAG}\nif [[ $check_flag != $CHECK_FLAG_ON ]]; then\nexit $EXIT_SUCCESS\nfi\n\n\n#######################################\n# check the file max size limit\n#######################################\n\n# get maximum filesize (from repository-specific config)\nmaxsize_mb=\"${REPO_MAX_FILE_SIZE}\"\n\nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\necho \"failed to get ${CONFIG_NAME} from config\"\nexit \"$EXIT_FAILURE\"\nfi\n\npush_size=\"0\"\n# read lines from stdin (format: \" \\n\")\nwhile read oldref newref refname; do\n# skip branch deletions\nif [[ \"$newref\" == \"$NULLSHA\" ]]; then\n continue\nfi\n\n# find large objects\n# check all objects from $oldref (possible $NULLSHA) to $newref, but\n# skip all objects that have already been accepted (i.e. are referenced by\n# another branch or tag).\n\nnew_branch_flag=0\nif [[ \"$oldref\" == \"$NULLSHA\" ]]; then\n target=\"$newref\"\n new_branch_flag=1\n echo \"You are creating a new remote branch,openI will check all files in commit history to find oversize files\"\nelse\n target=\"${oldref}..${newref}\"\nfi\nmaxsize=`expr $maxsize_mb \\* 1048576` \n\n# find objects in this push_size\n# print like:\n# 08da8e2ab9ae4095bf94dd71ac913132b880b463 commit 214\n# 43e993b768ede5740e8c65de2ed6edec25053ea1 tree 185\n# 4476971d76569039df7569af1b8d03c288f6b193 blob 20167318 b0417e6593a1.zip\nfiles=\"$(git rev-list --objects \"$target\" | \\\n git cat-file $'--batch-check=%%(objectname) %%(objecttype) %%(objectsize) %%(rest)' | \\\n awk -F ' ' -v maxbytes=\"$maxsize\" 'BEGIN {totalIn=0} {if( $3 > maxbytes && $2 == \"blob\") { totalIn+=$3; print $4} else { totalIn+=$3}} END { printf (\"totalIn=\\t%%s\",totalIn)}' )\"\n \nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\n echo \"failed to check for large files in ref ${refname}\"\n continue\nfi\n\nIFS=$'\\n'\n# rewrite IFS to seperate line in $files\nfor file in $files; do\n # if don't unset IFS,temp_array=(${file}) will get error answer\n \n if [[ ${file} == totalIn=* ]]; then\n\tIFS=$'\\t'\n\ttemp_array=(${file})\n\tpush_size=${temp_array[1]}\n\tcontinue\n fi\n\tunset IFS\n if [[ \"$status\" == $EXIT_SUCCESS ]]; then\n\t\techo -e \"Error: Your push was rejected because it contains files larger than $(numfmt --to=iec \"$maxsize_mb\") Mb\"\n\t\techo \"help document -- ...\"\n\t\techo \"oversize files:\"\n\t\tstatus=\"$EXIT_FAILURE\"\t\n fi\n echo -e \"\\033[31m- ${file}\\033[0m \"\ndone\n\nif [[ \"$status\" != $EXIT_SUCCESS ]]; then\n\texit \"$status\"\nfi\n\ndone\n\n#######################################\n# check the repo max size limit\n#######################################\nif [[ $push_size -eq \"0\" ]]; then\n\texit $EXIT_SUCCESS\nfi\n\n# if create new branch or tag,use count-objects -v to get pack size\nif [[ $new_branch_flag -eq 1 ]]; then\n size_kb=`git count-objects -v | grep 'size-pack' | sed 's/.*\\(size-pack:\\).//'`\n size_pack_kb=`git count-objects -v | grep 'size:' | sed 's/.*\\(size:\\).//'`\n\ttotal_kb=`expr $size_kb + $size_pack_kb`\n\tlet push_size=$total_kb*1024\nfi\n\nsizelimit_mb=\"${REPO_MAX_SIZE}\"\nlet sizelimit_b=$sizelimit_mb*1024*1024\n\n# repo size at here means the size of repo directory in server \nreposize_b=${REPO_CURRENT_SIZE}\n\nlet total=$REPO_CURRENT_SIZE*1024\n\nif [ $total -gt $sizelimit_b ]; then\n echo \"Error: Your push was rejected because the repository size is large than $sizelimit_mb Mb\"\n echo \"Error: 推送被拒绝,文件或目录大小大于 $sizelimit_mb Mb\"\n exit $EXIT_FAILURE\nfi\n\n\nexit $EXIT_SUCCESS", setting.ScriptType, LimitSize), + + fmt.Sprintf(""), + fmt.Sprintf(""), + } + + oldHookPath := filepath.Join(hookDir, hookName+".d", SIZE_LIMIT_SCRIPT_NAME) + if !com.IsExist(oldHookPath) { + //if err := checkHookFile(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0], results); err != nil { + // ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + // ctx.NotFound() + // return + //} + + if err := writeHookTpl(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0]); err != nil { + ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + ctx.NotFound() + return + } + } + ctx.Status(http.StatusOK) +} + +func resetRepoSizeLimitHook(ctx *context.APIContext) { + form := web.GetForm(ctx).(*gitea_api.RepositoryPayload) + eventName := string(form.Action) + if eventName == "deleted" { + ctx.Status(http.StatusOK) + return + } + getAssignmentRepo(ctx, form.Repository.ID) + hookDir := filepath.Join(ctx.Repo.Repository.RepoPath(), "hooks") + hookName := "pre-receive" + //results := make([]string, 0, 10) + sizeLimitTpls := []string{ + fmt.Sprintf("#!/usr/bin/env %s\n\n\nset -o pipefail\n\nreadonly DEFAULT_FILE_MAXSIZE_MB=\"30\" \nreadonly CONFIG_NAME=\"hooks.maxfilesize\"\nreadonly NULLSHA=\"0000000000000000000000000000000000000000\"\nreadonly EXIT_SUCCESS=0\nreadonly EXIT_FAILURE=1\nreadonly DEFAULT_REPO_MAXSIZE_MB=\"1024\" \nreadonly CHECK_FLAG_ON=1\n\n\nstatus=\"$EXIT_SUCCESS\"\n\n# skip this hook entirely if shell check is not open\ncheck_flag=${PUSH_SIZE_CHECK_FLAG}\nif [[ $check_flag != $CHECK_FLAG_ON ]]; then\nexit $EXIT_SUCCESS\nfi\n\n\n#######################################\n# check the file max size limit\n#######################################\n\n# get maximum filesize (from repository-specific config)\nmaxsize_mb=\"${REPO_MAX_FILE_SIZE}\"\n\nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\necho \"failed to get ${CONFIG_NAME} from config\"\nexit \"$EXIT_FAILURE\"\nfi\n\npush_size=\"0\"\n# read lines from stdin (format: \" \\n\")\nwhile read oldref newref refname; do\n# skip branch deletions\nif [[ \"$newref\" == \"$NULLSHA\" ]]; then\n continue\nfi\n\n# find large objects\n# check all objects from $oldref (possible $NULLSHA) to $newref, but\n# skip all objects that have already been accepted (i.e. are referenced by\n# another branch or tag).\n\nnew_branch_flag=0\nif [[ \"$oldref\" == \"$NULLSHA\" ]]; then\n target=\"$newref\"\n new_branch_flag=1\n echo \"You are creating a new remote branch,openI will check all files in commit history to find oversize files\"\nelse\n target=\"${oldref}..${newref}\"\nfi\nmaxsize=`expr $maxsize_mb \\* 1048576` \n\n# find objects in this push_size\n# print like:\n# 08da8e2ab9ae4095bf94dd71ac913132b880b463 commit 214\n# 43e993b768ede5740e8c65de2ed6edec25053ea1 tree 185\n# 4476971d76569039df7569af1b8d03c288f6b193 blob 20167318 b0417e6593a1.zip\nfiles=\"$(git rev-list --objects \"$target\" | \\\n git cat-file $'--batch-check=%%(objectname) %%(objecttype) %%(objectsize) %%(rest)' | \\\n awk -F ' ' -v maxbytes=\"$maxsize\" 'BEGIN {totalIn=0} {if( $3 > maxbytes && $2 == \"blob\") { totalIn+=$3; print $4} else { totalIn+=$3}} END { printf (\"totalIn=\\t%%s\",totalIn)}' )\"\n \nif [[ \"$?\" != $EXIT_SUCCESS ]]; then\n echo \"failed to check for large files in ref ${refname}\"\n continue\nfi\n\nIFS=$'\\n'\n# rewrite IFS to seperate line in $files\nfor file in $files; do\n # if don't unset IFS,temp_array=(${file}) will get error answer\n \n if [[ ${file} == totalIn=* ]]; then\n\tIFS=$'\\t'\n\ttemp_array=(${file})\n\tpush_size=${temp_array[1]}\n\tcontinue\n fi\n\tunset IFS\n if [[ \"$status\" == $EXIT_SUCCESS ]]; then\n\t\techo -e \"Error: Your push was rejected because it contains files larger than $(numfmt --to=iec \"$maxsize_mb\") Mb\"\n\t\techo \"help document -- https://openi.pcl.ac.cn/zeizei/OpenI_Learning/src/branch/master/docs/git/repository_capacity_help.md\"\n\t\techo \"oversize files:\"\n\t\tstatus=\"$EXIT_FAILURE\"\t\n fi\n echo -e \"\\033[31m- ${file}\\033[0m \"\ndone\n\nif [[ \"$status\" != $EXIT_SUCCESS ]]; then\n\texit \"$status\"\nfi\n\ndone\n\n#######################################\n# check the repo max size limit\n#######################################\nif [[ $push_size -eq \"0\" ]]; then\n\texit $EXIT_SUCCESS\nfi\n\n# if create new branch or tag,use count-objects -v to get pack size\nif [[ $new_branch_flag -eq 1 ]]; then\n size_kb=`git count-objects -v | grep 'size-pack' | sed 's/.*\\(size-pack:\\).//'`\n size_pack_kb=`git count-objects -v | grep 'size:' | sed 's/.*\\(size:\\).//'`\n\ttotal_kb=`expr $size_kb + $size_pack_kb`\n\tlet push_size=$total_kb*1024\nfi\n\nsizelimit_mb=\"${REPO_MAX_SIZE}\"\nlet sizelimit_b=$sizelimit_mb*1024*1024\n\n# repo size at here means the size of repo directory in server \nreposize_b=${REPO_CURRENT_SIZE}\n\ntotal=`expr $push_size + $reposize_b`\n\nif [ $total -gt $sizelimit_b ]; then\n echo \"Error: Your push was rejected because the repository size is large than $sizelimit_mb Mb\"\n echo \"see the help document--https://openi.pcl.ac.cn/zeizei/OpenI_Learning/src/branch/master/docs/git/repository_capacity_help.md\"\n exit $EXIT_FAILURE\nfi\n\n\nexit $EXIT_SUCCESS", setting.ScriptType), + fmt.Sprintf(""), + fmt.Sprintf(""), + } + + oldHookPath := filepath.Join(hookDir, hookName+".d", SIZE_LIMIT_SCRIPT_NAME) + if !com.IsExist(oldHookPath) { + //if err := checkHookFile(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0], results); err != nil { + // ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + // ctx.NotFound() + // return + //} + + if err := writeHookTpl(generateHookScriptPath(hookDir, hookName, SIZE_LIMIT_SCRIPT_NAME), sizeLimitTpls[0]); err != nil { + ctx.Error(http.StatusInternalServerError, "CreateSizeLimitHook checkHookFile", err) + ctx.NotFound() + return + } + } + ctx.Status(http.StatusOK) +} + +func getAssignmentRepo(ctx *context.APIContext, id int64) { + // Get repository. + repo, err := repo_model.GetRepositoryByID(ctx, id) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + redirectRepoID, err := repo_model.LookupRedirect(repo.OwnerID, repo.Name) + if err == nil { + context.RedirectToRepo(ctx.Base, redirectRepoID) + } else if repo_model.IsErrRedirectNotExist(err) { + ctx.NotFound(ctx.Tr("repo.RepoNotFound", repo.OwnerID)) + } else { + ctx.Error(http.StatusInternalServerError, "LookupRepoRedirect", err) + } + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + } + return + } + ctx.Repo.Repository = repo +} + +func getAssignmentRepoByName(ctx *context.APIContext, ownerName string, repoName string) { + // Get repository. + owner, err := user_model.GetUserByName(ctx, ownerName) + repo, err := repo_model.GetRepositoryByName(owner.ID, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + redirectRepoID, err := repo_model.LookupRedirect(repo.OwnerID, repo.Name) + if err == nil { + context.RedirectToRepo(ctx.Base, redirectRepoID) + } else if repo_model.IsErrRedirectNotExist(err) { + ctx.NotFound(ctx.Tr("repo.RepoNotFound", repo.OwnerID)) + } else { + ctx.Error(http.StatusInternalServerError, "LookupRepoRedirect", err) + } + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + } + return + } + ctx.Repo.Repository = repo +} + +// UpdateDelegateHooks creates all the hooks scripts for the repo +func UpdateDelegateHooks(repoPath string) (err error) { + hookNames, hookTpls, giteaHookTpls := getHookTemplates() + hookDir := filepath.Join(repoPath, "hooks") + + for i, hookName := range hookNames { + oldHookPath := filepath.Join(hookDir, hookName) + newHookPath := filepath.Join(hookDir, hookName+".d", "gitea") + + if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil { + return fmt.Errorf("create hooks dir '%s': %w", filepath.Join(hookDir, hookName+".d"), err) + } + + // WARNING: This will override all old server-side hooks + if err = util.Remove(oldHookPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to pre-remove old hook file '%s' prior to rewriting: %w ", oldHookPath, err) + } + if err = os.WriteFile(oldHookPath, []byte(hookTpls[i]), 0o777); err != nil { + return fmt.Errorf("write old hook file '%s': %w", oldHookPath, err) + } + + if err = ensureExecutable(oldHookPath); err != nil { + return fmt.Errorf("Unable to set %s executable. Error %w", oldHookPath, err) + } + + if err = util.Remove(newHookPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to pre-remove new hook file '%s' prior to rewriting: %w", newHookPath, err) + } + if err = os.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0o777); err != nil { + return fmt.Errorf("write new hook file '%s': %w", newHookPath, err) + } + + if err = ensureExecutable(newHookPath); err != nil { + return fmt.Errorf("Unable to set %s executable. Error %w", oldHookPath, err) + } + } + + return nil +} + +func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) { + hookNames = []string{"pre-receive", "update", "post-receive"} + hookTpls = []string{ + // for pre-receive + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)/..} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do + test -x "${hook}" && test -f "${hook}" + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do + [ ${i} -eq 0 ] || exit ${i} +done +`, setting.ScriptType), + + // for update + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0/..)} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do + test -x "${hook}" && test -f "${hook}" + "${hook}" $1 $2 $3 + exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do + [ ${i} -eq 0 ] || exit ${i} +done +`, setting.ScriptType), + + // for post-receive + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)/..} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do + test -x "${hook}" && test -f "${hook}" + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do + [ ${i} -eq 0 ] || exit ${i} +done +`, setting.ScriptType), + } + + giteaHookTpls = []string{ + // for pre-receive + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +%s hook --config=%s pre-receive +`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), + + // for update + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +%s hook --config=%s update $1 $2 $3 +`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), + + // for post-receive + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +%s hook --config=%s post-receive +`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), + } + + if git.SupportProcReceive { + hookNames = append(hookNames, "proc-receive") + hookTpls = append(hookTpls, + fmt.Sprintf(`#!/usr/bin/env %s +# AUTO GENERATED BY GITEA, DO NOT MODIFY +%s hook --config=%s proc-receive +`, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf))) + giteaHookTpls = append(giteaHookTpls, "") + } + + return hookNames, hookTpls, giteaHookTpls +} + +func generateHookScriptPath(hookDir, hookName, fileName string) string { + return filepath.Join(hookDir, hookName+".d", fileName) +} + +func checkHookFile(filePath, tpl string, results []string) error { + if tpl == "" { + return nil + } + contents, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + if string(contents) != tpl { + results = append(results, fmt.Sprintf("old hook file %s is out of date", filePath)) + } + if !checkExecutable(filePath) { + results = append(results, fmt.Sprintf("old hook file %s is not executable", filePath)) + } + return nil +} + +func writeHookTpl(hookPath, content string) error { + if content == "" { + return nil + } + if err := ioutil.WriteFile(hookPath, []byte(content), 0777); err != nil { + return fmt.Errorf("write new hook file '%s': %v", hookPath, err) + } + + if err := ensureExecutable(hookPath); err != nil { + return fmt.Errorf("Unable to set %s executable. Error %v", hookPath, err) + } + return nil +} + +func checkExecutable(filename string) bool { + fileInfo, err := os.Stat(filename) + if err != nil { + return false + } + return (fileInfo.Mode() & 0100) > 0 +} + +func ensureExecutable(filename string) error { + fileInfo, err := os.Stat(filename) + if err != nil { + return err + } + if (fileInfo.Mode() & 0100) > 0 { + return nil + } + mode := fileInfo.Mode() | 0100 + return os.Chmod(filename, mode) +} diff --git a/routers/hat/repo/wiki.go b/routers/hat/repo/wiki.go index 1f0e013..1bf58a0 100644 --- a/routers/hat/repo/wiki.go +++ b/routers/hat/repo/wiki.go @@ -2,12 +2,15 @@ package repo import ( "net/http" + "net/url" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" util "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/convert" wiki_service "code.gitea.io/gitea/services/wiki" ) @@ -73,3 +76,110 @@ func ListWikiPageNames(ctx *context.APIContext) { ctx.SetTotalCountHeader(int64(len(entries))) ctx.JSON(http.StatusOK, pages) } + +func ShowRevision(ctx *context.APIContext) { + wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath()) + if err != nil { + if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { + ctx.NotFound(err) + } else { + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + } + return + } + + if wikiRepo != nil { + + defer wikiRepo.Close() + } + + // get requested pagename + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + if len(pageName) == 0 { + pageName = "Home" + } + + revision := ctx.PathParamRaw(":revision") + + commit, err := wikiRepo.GetCommit(revision) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCommit", err) + return + } + + content, pageFilename := wikiContentsByName(ctx, commit, pageName, false) + + sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true) + + footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true) + + // get commit count - wiki revisions + commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + + // Get last change information. + lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err) + return + } + + ctx.JSON(http.StatusOK, &api.WikiPage{ + WikiPageMetaData: convert.ToWikiPageMetaData(pageName, lastCommit, ctx.Repo.Repository), + ContentBase64: content, + CommitCount: commitsCount, + Sidebar: sidebarContent, + Footer: footerContent, + }) + +} + +// findEntryForFile finds the tree entry for a target filepath. +func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { + entry, err := commit.GetTreeEntryByPath(target) + if err != nil { + return nil, err + } + if entry != nil { + return entry, nil + } + + // Then the unescaped, shortest alternative + var unescapedTarget string + if unescapedTarget, err = url.QueryUnescape(target); err != nil { + return nil, err + } + return commit.GetTreeEntryByPath(unescapedTarget) +} + +// wikiContentsByEntry returns the contents of the wiki page referenced by the +// given tree entry, encoded with base64. Writes to ctx if an error occurs. +func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { + blob := entry.Blob() + if blob.Size() > setting.API.DefaultMaxBlobSize { + return "" + } + content, err := blob.GetBlobContentBase64() + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetBlobContentBase64", err) + return "" + } + return content +} + +// wikiContentsByName returns the contents of a wiki page, along with a boolean +// indicating whether the page exists. Writes to ctx if an error occurs. +func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) { + gitFilename := wiki_service.WebPathToGitPath(wikiName) + entry, err := findEntryForFile(commit, gitFilename) + if err != nil { + if git.IsErrNotExist(err) { + if !isSidebarOrFooter { + ctx.NotFound() + } + } else { + ctx.ServerError("findEntryForFile", err) + } + return "", "" + } + return wikiContentsByEntry(ctx, entry), gitFilename +} diff --git a/routers/hat/user/user.go b/routers/hat/user/user.go index b933c91..9f9b558 100644 --- a/routers/hat/user/user.go +++ b/routers/hat/user/user.go @@ -1,10 +1,18 @@ package user import ( + "fmt" + "math" "net/http" + "strconv" + + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" + code_indexer "code.gitea.io/gitea/modules/indexer/code" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + hat_activities_models "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/activities" ) @@ -25,3 +33,91 @@ func GetUserHeatmapData(ctx *context.APIContext) { ctx.JSON(http.StatusOK, heatmap) } + +func Code(ctx *context.APIContext) { + language := ctx.FormString("language") + keyword := ctx.FormString("keyword") + queryType := ctx.FormString("query_type") + isMatch := queryType == "match" + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + var ( + repoIDs []int64 + err error + ) + + repoIDs, err = repo_model.FindUserCodeAccessibleOwnerRepoIDs(ctx, ctx.ContextUser.ID, ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindUserCodeAccessibleOwnerRepoIDs", err) + return + } + + var ( + total int + searchResults []*code_indexer.Result + searchResultLanguages []*code_indexer.SearchResultLanguages + ) + + var codeIndexerUnavailable bool + var repoMaps map[int64]*repo_model.Repository + + if len(repoIDs) > 0 { + total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) + if err != nil { + if code_indexer.IsAvailable(ctx) { + ctx.Error(http.StatusInternalServerError, "SearchResults", err) + return + } + codeIndexerUnavailable = true + } else { + codeIndexerUnavailable = !code_indexer.IsAvailable(ctx) + } + + loadRepoIDs := make([]int64, 0, len(searchResults)) + for _, result := range searchResults { + var find bool + for _, id := range loadRepoIDs { + if id == result.RepoID { + find = true + break + } + } + if !find { + loadRepoIDs = append(loadRepoIDs, result.RepoID) + } + } + + repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetRepositoriesMapByIDs", err) + return + } + + repoMaps = repoMaps + } + + pageCount := int(math.Ceil(float64(total) / float64(setting.UI.RepoSearchPagingNum))) + ctx.Resp.Header().Set("X-Page", strconv.Itoa(page)) + ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(setting.UI.RepoSearchPagingNum)) + ctx.Resp.Header().Set("X-Total", strconv.FormatInt(int64(total), 10)) + ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount)) + ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount)) + + ctx.SetLinkHeader(int(total), setting.UI.RepoSearchPagingNum) + ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", total)) + ctx.JSON(http.StatusOK, struct { + CodeIndexerUnavailable bool + RepoMaps map[int64]*repo_model.Repository + SearchResults []*code_indexer.Result + SearchResultLanguages []*code_indexer.SearchResultLanguages + }{ + CodeIndexerUnavailable: codeIndexerUnavailable, + RepoMaps: repoMaps, + SearchResults: searchResults, + SearchResultLanguages: searchResultLanguages, + }) +} diff --git a/routers/hat/web/web.go b/routers/hat/web/web.go index d25977a..24ad47f 100644 --- a/routers/hat/web/web.go +++ b/routers/hat/web/web.go @@ -282,8 +282,10 @@ func registerRoutes(m *web.Route) { reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) // TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView) - ignSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) - ignExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) + //ignSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) + ignSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) + //ignExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) + ignExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) validation.AddBindingRules() diff --git a/services/actions/secrets.go b/services/actions/secrets.go new file mode 100644 index 0000000..44dd92d --- /dev/null +++ b/services/actions/secrets.go @@ -0,0 +1,105 @@ +package actions + +import ( + "context" + "regexp" + "strings" + "sync" + + secret_model "code.gitea.io/gitea/models/secret" + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + + "code.gitea.io/gitea/modules/util" +) + +var globalVars = sync.OnceValue(func() (ret struct { + namePattern, forbiddenPrefixPattern *regexp.Regexp +}, +) { + ret.namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") + ret.forbiddenPrefixPattern = regexp.MustCompile("(?i)^GIT(EA|HUB)_") + return ret +}) + +func ValidateName(name string) error { + vars := globalVars() + if !vars.namePattern.MatchString(name) || + vars.forbiddenPrefixPattern.MatchString(name) || + strings.EqualFold(name, "CI") /* CI is always set to true in GitHub Actions*/ { + return util.NewInvalidArgumentErrorf("invalid variable or secret name") + } + return nil +} + +func CreateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, error) { + if err := ValidateName(name); err != nil { + return nil, err + } + + s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data) + if err != nil { + return nil, err + } + + return s, nil +} + +func UpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (bool, error) { + if err := ValidateName(name); err != nil { + return false, err + } + + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return false, err + } + + if s == nil { + return false, util.NewNotExistErrorf("secret not found") + } + + err = secret_model.UpdateSecret(ctx, s.ID, data) + if err != nil { + return false, err + } + + return true, nil +} + +func GetSecret(ctx context.Context, opts hat_actions_model.FindSecretsOptions) (*secret_model.Secret, error) { + vars, err := hat_actions_model.FindSecrets(ctx, opts) + if err != nil { + return nil, err + } + if len(vars) != 1 { + return nil, util.NewNotExistErrorf("variable not found") + } + return vars[0], nil +} + +func DeleteSecretByName(ctx context.Context, ownerID, repoID int64, name string) error { + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return err + } + + return hat_actions_model.DeleteSecret(ctx, s) +} + +func DeleteSecretByID(ctx context.Context, secretID int64) error { + s, err := GetSecret(ctx, hat_actions_model.FindSecretsOptions{ + SecretID: secretID, + }) + if err != nil { + return err + } + return hat_actions_model.DeleteSecret(ctx, s) +} diff --git a/services/actions/variables.go b/services/actions/variables.go new file mode 100644 index 0000000..31b8cd1 --- /dev/null +++ b/services/actions/variables.go @@ -0,0 +1,69 @@ +package actions + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/util" + + secret_service "code.gitea.io/gitea/services/secrets" + hat_actions_model "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/actions" + hat_util "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/util" +) + +func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) { + if err := secret_service.ValidateName(name); err != nil { + return nil, err + } + + v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, hat_util.ReserveLineBreakForTextarea(data)) + if err != nil { + return nil, err + } + + return v, nil +} + +func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { + if err := secret_service.ValidateName(variable.Name); err != nil { + return false, err + } + + variable.Data = hat_util.ReserveLineBreakForTextarea(variable.Data) + + return actions_model.UpdateVariable(ctx, variable) +} + +func DeleteVariableByID(ctx context.Context, variableID int64) error { + v, err := GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + IDs: []int64{variableID}, + }) + if err != nil { + return err + } + return hat_actions_model.DeleteVariable(ctx, v) +} + +func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error { + v, err := GetVariable(ctx, hat_actions_model.FindVariablesOpts{ + OwnerID: ownerID, + RepoID: repoID, + Name: name, + }) + if err != nil { + return err + } + + return hat_actions_model.DeleteVariable(ctx, v) +} + +func GetVariable(ctx context.Context, opts hat_actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) { + vars, err := hat_actions_model.FindVariables(ctx, opts) + if err != nil { + return nil, err + } + if len(vars) != 1 { + return nil, util.NewNotExistErrorf("variable not found") + } + return vars[0], nil +}