From 25887e01ebc87c75ba666b5853ca4fac95826320 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Mon, 6 May 2024 21:44:39 +0200 Subject: [PATCH] build: rework schema gen (#5) * build: refactor paths for code-gen * build: generate attribute docs automatically * build: remove dead headlines * build: remove verbose headlines * ci: fix missing import * add docs for all attributes * ci: sort attributes --- .gitignore | 2 +- README.md | 215 +++++++------ pkg/scm/gitlab/context.go | 6 +- schema/docs.tmpl | 9 + schema/generate.go | 3 + schema/gitlab.go | 291 ++++++++++++++++++ .../{gitlab/gqlgen.yml => gitlab.gqlgen.yml} | 4 +- schema/gitlab.schema.graphqls | 246 +++++++++++++++ schema/gitlab/generate.go | 3 - schema/gitlab/main.go | 111 ------- schema/gitlab/schema.graphqls | 110 ------- 11 files changed, 658 insertions(+), 342 deletions(-) create mode 100644 schema/docs.tmpl create mode 100644 schema/generate.go create mode 100644 schema/gitlab.go rename schema/{gitlab/gqlgen.yml => gitlab.gqlgen.yml} (97%) create mode 100644 schema/gitlab.schema.graphqls delete mode 100644 schema/gitlab/generate.go delete mode 100644 schema/gitlab/main.go delete mode 100644 schema/gitlab/schema.graphqls diff --git a/.gitignore b/.gitignore index d650505..3e073bf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ bin/ completions/ coverage.txt dist/ -/schema/*/ignore +/schema/ignore manpages/ scm-engine scm-engine.exe diff --git a/README.md b/README.md index 4a79c37..b72eef8 100644 --- a/README.md +++ b/README.md @@ -37,20 +37,7 @@ - [`label.skip_if` (optional)](#labelskip_if-optional) - [Expr-lang information](#expr-lang-information) - [Attributes](#attributes) - - [project](#project) - - [project.labels](#projectlabels) - - [group](#group) - - [merge_request](#merge_request) - - [merge_request.diff_stats](#merge_requestdiff_stats) - - [merge_request.first_commit](#merge_requestfirst_commit) - - [merge_request.last_commit](#merge_requestlast_commit) - - [merge_request.labels](#merge_requestlabels) - [Functions](#functions) - - [`merge_request.modified_files`](#merge_requestmodified_files) - - [`duration`](#duration) - - [`uniq`](#uniq) - - [`filepath_dir`](#filepath_dir) - - [`limit_path_depth_to`](#limit_path_depth_to) ## Installation @@ -360,7 +347,7 @@ The `skip_if` field must be a valid [Expr-lang](https://expr-lang.org/) expressi ### Attributes > [!NOTE] -> Missing an attribute? The `schema/gitlab/schema.graphqls` file are what is used to query GitLab, adding the missing `field` to the right `type` should make it accessible. +> Missing an attribute? The `schema/gitlab.schema.graphqls` file are what is used to query GitLab, adding the missing `field` to the right `type` should make it accessible. > Please open an issue or Pull Request if something is missing. > [!IMPORTANT] @@ -370,104 +357,108 @@ The following attributes are available in `script` fields. They can be accessed exactly as shown in this list. -#### project - -> [!NOTE] -> See the [GitLab GraphQL `Project` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#project) for more details about the fields. - -- `project.archived` (boolean) -- `project.created_at` (time) -- `project.description` (string) -- `project.full_path` (string) -- `project.id` (string) -- `project.last_activity_at` (time) -- `project.name_with_namespace` (string) -- `project.name` (string) -- `project.path` (string) -- `project.topics[]` (array of string) -- `project.visibility` (string) - -#### project.labels - -> [!NOTE] -> See the [GitLab GraphQL `Label` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#label) for more details about the fields. - -- `project.labels[].color` (string) -- `project.labels[].description` (string) -- `project.labels[].id` (string) -- `project.labels[].title` (string) - -#### group - -> See the [GitLab GraphQL `Group` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#group) for more details about the fields. - -- `group.description` (string) -- `group.id` (string) -- `group.name` (string) - -#### merge_request - -> See the [GitLab GraphQL `MergeRequest` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#mergerequest) for more details about the fields. - -- `merge_request.approvals_left` (int) -- `merge_request.approvals_required` (int) -- `merge_request.approved` (boolean) -- `merge_request.auto_merge_enabled` (int) -- `merge_request.auto_merge_strategy` (string) -- `merge_request.conflicts` (bool) -- `merge_request.created_at` (time) -- `merge_request.description` (string) -- `merge_request.diverged_from_target_branch` (bool) -- `merge_request.draft` (boolean) -- `merge_request.id` (string) -- `merge_request.iid` (string) -- `merge_request.merge_status_enum` (string) -- `merge_request.mergeable` (boolean) -- `merge_request.merged_at` (optional, time) -- `merge_request.source_branch_exists` (boolean) -- `merge_request.source_branch_protected` (boolean) -- `merge_request.source_branch` (string) -- `merge_request.squash_on_merge` (boolean) -- `merge_request.squash` (boolean) -- `merge_request.state` (string) -- `merge_request.target_branch_exists` (string) -- `merge_request.target_branch` (string) -- `merge_request.time_between_first_and_last_commit` (duration) - SCM Engine - The `duration()` between the first and last commit in the Merge Request. -- `merge_request.time_since_first_commit` (duration) - SCM Engine - The `duration()` between `now()` and the first commit in the Merge Request. -- `merge_request.time_since_last_commit` (duration) - SCM Engine - The `duration()` between `now()` and the last commit in the Merge Request. -- `merge_request.title` (string) -- `merge_request.updated_at` (time) - -#### merge_request.diff_stats - -> See the [GitLab GraphQL `DiffStats` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#diffstats) for more details about the fields. - -- `merge_request.diff_stats[].additions` (int) -- `merge_request.diff_stats[].deletions` (int) -- `merge_request.diff_stats[].path` (string) - -#### merge_request.first_commit - -> See the [GitLab GraphQL `Commit` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#commit) for more details about the fields. - -- `merge_request.first_commit.author_email` (string) -- `merge_request.first_commit.committed_date` (string) - -#### merge_request.last_commit - -> See the [GitLab GraphQL `Commit` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#commit) for more details about the fields. - -- `merge_request.last_commit.author_email` (string) -- `merge_request.last_commit.committed_date` (string) - -#### merge_request.labels - -> See the [GitLab GraphQL `Label` GraphQL resource](https://docs.gitlab.com/ee/api/graphql/reference/#label) for more details about the fields. - -- `merge_request.labels[].color` (string) -- `merge_request.labels[].description` (string) -- `merge_request.labels[].id` (string) -- `merge_request.labels[].title` (string) +- `group.description` (string) Description of the namespace +- `group.emails_disabled` (optional bool) Indicates if a group has email notifications disabled +- `group.emails_enabled` (optional bool) Indicates if a group has email notifications enabled +- `group.full_name` (string) Full name of the namespace +- `group.full_path` (string) Full path of the namespace +- `group.id` (string) ID of the namespace +- `group.mentions_disabled` (optional bool) Indicates if a group is disabled from getting mentioned +- `group.name` (string) Name of the namespace +- `group.path` (string) Path of the namespace +- `group.visibility` (optional string) Visibility of the namespace +- `group.web_url` (string) Web URL of the group +- `merge_request.approvals_left` (optional int) Number of approvals left +- `merge_request.approvals_required` (optional int) Number of approvals required +- `merge_request.approved` (bool) Indicates if the merge request has all the required approvals +- `merge_request.auto_merge_enabled` (bool) Indicates if auto merge is enabled for the merge request +- `merge_request.auto_merge_strategy` (optional string) Selected auto merge strategy +- `merge_request.commit_count` (optional int) Number of commits in the merge request +- `merge_request.conflicts` (bool) Indicates if the merge request has conflicts +- `merge_request.created_at` (time) Timestamp of when the merge request was created +- `merge_request.description` (optional string) Description of the merge request (Markdown rendered as HTML for caching) +- `merge_request.diff_stats[].additions` (int) Number of lines added to this file +- `merge_request.diff_stats[].deletions` (int) Number of lines deleted from this file +- `merge_request.diff_stats[].path` (string) File path, relative to repository root +- `merge_request.discussion_locked` (bool) Indicates if comments on the merge request are locked to members only +- `merge_request.diverged_from_target_branch` (bool) Indicates if the source branch is behind the target branch +- `merge_request.downvotes` (int) Number of downvotes for the merge request +- `merge_request.draft` (bool) Indicates if the merge request is a draft +- `merge_request.first_commit.author_email` (optional string) Commit author’s email +- `merge_request.first_commit.author_name` (optional string) Commit authors name +- `merge_request.first_commit.authored_date` (optional time) Timestamp of when the commit was authored +- `merge_request.first_commit.committed_date` (optional time) Timestamp of when the commit was committed +- `merge_request.first_commit.committer_email` (optional string) Email of the committer +- `merge_request.first_commit.committer_name` (optional string) Name of the committer +- `merge_request.first_commit.description` (optional string) Description of the commit message +- `merge_request.first_commit.full_title` (optional string) Full title of the commit message +- `merge_request.first_commit.id` (optional string) ID (global ID) of the commit +- `merge_request.first_commit.message` (optional string) Raw commit message +- `merge_request.first_commit.sha` (string) SHA1 ID of the commit +- `merge_request.first_commit.short_id` (string) Short SHA1 ID of the commit +- `merge_request.first_commit.title` (optional string) Title of the commit message +- `merge_request.first_commit.web_url` (string) Web URL of the commit +- `merge_request.force_remove_source_branch` (optional bool) Indicates if the project settings will lead to source branch deletion after merge +- `merge_request.id` (string) ID of the merge request +- `merge_request.iid` (string) Internal ID of the merge request +- `merge_request.labels[].color` (string) Background color of the label +- `merge_request.labels[].description` (string) Description of the label (Markdown rendered as HTML for caching) +- `merge_request.labels[].id` (string) Label ID +- `merge_request.labels[].title` (string) Content of the label +- `merge_request.last_commit.author_email` (optional string) Commit author’s email +- `merge_request.last_commit.author_name` (optional string) Commit authors name +- `merge_request.last_commit.authored_date` (optional time) Timestamp of when the commit was authored +- `merge_request.last_commit.committed_date` (optional time) Timestamp of when the commit was committed +- `merge_request.last_commit.committer_email` (optional string) Email of the committer +- `merge_request.last_commit.committer_name` (optional string) Name of the committer +- `merge_request.last_commit.description` (optional string) Description of the commit message +- `merge_request.last_commit.full_title` (optional string) Full title of the commit message +- `merge_request.last_commit.id` (optional string) ID (global ID) of the commit +- `merge_request.last_commit.message` (optional string) Raw commit message +- `merge_request.last_commit.sha` (string) SHA1 ID of the commit +- `merge_request.last_commit.short_id` (string) Short SHA1 ID of the commit +- `merge_request.last_commit.title` (optional string) Title of the commit message +- `merge_request.last_commit.web_url` (string) Web URL of the commit +- `merge_request.merge_status_enum` (string) Merge status of the merge request +- `merge_request.merge_when_pipeline_succeeds` (optional bool) Indicates if the merge has been set to auto-merge +- `merge_request.mergeable` (bool) Indicates if the merge request is mergeable +- `merge_request.mergeable_discussions_state` (optional bool) Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged +- `merge_request.merged_at` (optional time) Timestamp of when the merge request was merged, null if not merged +- `merge_request.prepared_at` (optional time) Timestamp of when the merge request was prepared +- `merge_request.should_be_rebased` (bool) Indicates if the merge request will be rebased +- `merge_request.should_remove_source_branch` (optional bool) Indicates if the source branch of the merge request will be deleted after merge +- `merge_request.source_branch` (string) Source branch of the merge request +- `merge_request.source_branch_exists` (bool) Indicates if the source branch of the merge request exists +- `merge_request.source_branch_protected` (bool) Indicates if the source branch is protected +- `merge_request.squash` (bool) Indicates if the merge request is set to be squashed when merged. Project settings may override this value. Use squash_on_merge instead to take project squash options into account +- `merge_request.squash_on_merge` (bool) Indicates if the merge request will be squashed when merged +- `merge_request.state` (string) State of the merge request +- `merge_request.target_branch` (string) Target branch of the merge request +- `merge_request.target_branch_exists` (bool) Indicates if the target branch of the merge request exists +- `merge_request.time_between_first_and_last_commit` (optional duration) +- `merge_request.time_since_first_commit` (optional duration) +- `merge_request.time_since_last_commit` (optional duration) +- `merge_request.title` (string) Title of the merge request +- `merge_request.updated_at` (time) Timestamp of when the merge request was last updated +- `merge_request.upvotes` (int) Number of upvotes for the merge request. +- `merge_request.user_discussions_count` (optional int) Number of user discussions in the merge request +- `merge_request.user_notes_count` (optional int) User notes count of the merge request +- `project.archived` (bool) Indicates the archived status of the project +- `project.created_at` (time) Timestamp of the project creation +- `project.description` (string) Short description of the project +- `project.full_path` (string) Full path of the project +- `project.id` (string) ID of the project +- `project.issues_enabled` (bool) Indicates if Issues are enabled for the current user +- `project.labels[].color` (string) Background color of the label +- `project.labels[].description` (string) Description of the label (Markdown rendered as HTML for caching) +- `project.labels[].id` (string) Label ID +- `project.labels[].title` (string) Content of the label +- `project.last_activity_at` (time) Timestamp of the project last activity +- `project.name` (string) Name of the project (without namespace) +- `project.name_with_namespace` (string) Full name of the project with its namespace +- `project.path` (string) Path of the project +- `project.topics` ([]string) List of project topics +- `project.visibility` (string) Visibility of the project ### Functions diff --git a/pkg/scm/gitlab/context.go b/pkg/scm/gitlab/context.go index 937cac0..7d1011c 100644 --- a/pkg/scm/gitlab/context.go +++ b/pkg/scm/gitlab/context.go @@ -57,7 +57,7 @@ func NewContext(ctx context.Context, baseURL, token string) (*Context, error) { if len(evalContext.MergeRequest.ResponseFirstCommits.Nodes) > 0 { evalContext.MergeRequest.FirstCommit = &evalContext.MergeRequest.ResponseFirstCommits.Nodes[0] - tmp := time.Since(evalContext.MergeRequest.FirstCommit.CommittedDate) + tmp := time.Since(*evalContext.MergeRequest.FirstCommit.CommittedDate) evalContext.MergeRequest.TimeSinceFirstCommit = &tmp } @@ -66,14 +66,14 @@ func NewContext(ctx context.Context, baseURL, token string) (*Context, error) { if len(evalContext.MergeRequest.ResponseLastCommits.Nodes) > 0 { evalContext.MergeRequest.LastCommit = &evalContext.MergeRequest.ResponseLastCommits.Nodes[0] - tmp := time.Since(evalContext.MergeRequest.LastCommit.CommittedDate) + tmp := time.Since(*evalContext.MergeRequest.LastCommit.CommittedDate) evalContext.MergeRequest.TimeSinceLastCommit = &tmp } evalContext.MergeRequest.ResponseLastCommits = nil if evalContext.MergeRequest.FirstCommit != nil && evalContext.MergeRequest.LastCommit != nil { - tmp := evalContext.MergeRequest.FirstCommit.CommittedDate.Sub(evalContext.MergeRequest.LastCommit.CommittedDate).Round(time.Hour) + tmp := evalContext.MergeRequest.FirstCommit.CommittedDate.Sub(*evalContext.MergeRequest.LastCommit.CommittedDate).Round(time.Hour) evalContext.MergeRequest.TimeBetweenFirstAndLastCommit = &tmp } diff --git a/schema/docs.tmpl b/schema/docs.tmpl new file mode 100644 index 0000000..4c4e093 --- /dev/null +++ b/schema/docs.tmpl @@ -0,0 +1,9 @@ +{{- define "attributes" -}} +{{- range .Attributes -}} +{{ if .IsCustomType -}}{{- template "attributes" . }}{{- else }} +- `{{ .BlockName }}` ({{ if .Optional }}optional {{ end }}{{ .Type }}) {{ .Description }}{{- end -}} +{{- end }} +{{- end -}} + +### Attributes +{{ template "attributes" . -}} diff --git a/schema/generate.go b/schema/generate.go new file mode 100644 index 0000000..5c3418d --- /dev/null +++ b/schema/generate.go @@ -0,0 +1,3 @@ +package schema + +//go:generate go run gitlab.go diff --git a/schema/gitlab.go b/schema/gitlab.go new file mode 100644 index 0000000..c8c3de0 --- /dev/null +++ b/schema/gitlab.go @@ -0,0 +1,291 @@ +//go:build ignore + +package main + +import ( + "bytes" + "cmp" + _ "embed" + "fmt" + "html/template" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "github.com/99designs/gqlgen/api" + "github.com/99designs/gqlgen/codegen/config" + "github.com/99designs/gqlgen/plugin/modelgen" + "github.com/fatih/structtag" + "github.com/iancoleman/strcase" + "github.com/vektah/gqlparser/v2/ast" +) + +//go:embed docs.tmpl +var docs string + +var ( + Props = []*Property{} + PropMap = map[string]*Property{} +) + +func main() { + PropMap = make(map[string]*Property) + + cfg, err := config.LoadConfig(getRootPath() + "/schema/gitlab.gqlgen.yml") + if err != nil { + fmt.Fprintln(os.Stderr, "failed to load config", err.Error()) + + os.Exit(2) + } + + // Attaching the mutation function onto modelgen plugin + p := modelgen.Plugin{ + FieldHook: constraintFieldHook, + MutateHook: mutateHook, + } + + err = api.Generate(cfg, api.ReplacePlugin(&p)) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(3) + } + + nest(Props) + + var index bytes.Buffer + tmpl := template.Must(template.New("index").Parse(docs)) + + if err := tmpl.Execute(&index, Props[0]); err != nil { + panic(err) + } + + fmt.Println(index.String()) +} + +func nest(props []*Property) { + for _, field := range props { + if field.IsCustomType { + for _, nested := range PropMap[field.Type].Attributes { + field.AddAttribute(&Property{ + Name: nested.Name, + Description: nested.Description, + Optional: nested.Optional, + Type: nested.Type, + IsSlice: nested.IsSlice, + IsCustomType: nested.IsCustomType, + }) + } + } + + nest(field.Attributes) + } +} + +func getRootPath() string { + path, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + panic(err) + } + + return strings.TrimSpace(string(path)) +} + +// Defining mutation function +func constraintFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *modelgen.Field) (*modelgen.Field, error) { + // Call default hook to proceed standard directives like goField and goTag. + // You can omit it, if you don't need. + if f, err := modelgen.DefaultFieldMutateHook(td, fd, f); err != nil { + return f, err + } + + tags, err := structtag.Parse(f.Tag) + if err != nil { + return nil, err + } + + // Remove JSON tag, we don't need it + tags.Delete("json") + + if c := fd.Directives.ForName("internal"); c != nil { + tags.Set(&structtag.Tag{Key: "expr", Name: "-"}) + } else if c := fd.Directives.ForName("expr"); c != nil { + value := c.Arguments.ForName("key") + + if value != nil { + tags.Set(&structtag.Tag{Key: "expr", Name: value.Value.Raw}) + } + } + + if c := fd.Directives.ForName("generated"); c != nil { + tags.Set(&structtag.Tag{Key: "graphql", Name: "-"}) + } else if c := fd.Directives.ForName("graphql"); c != nil { + value := c.Arguments.ForName("key") + + if value != nil { + tags.Set(&structtag.Tag{Key: "graphql", Name: value.Value.Raw}) + } + } + + f.Tag = tags.String() + + return f, nil +} + +func mutateHook(b *modelgen.ModelBuild) *modelgen.ModelBuild { + for _, model := range b.Models { + modelName := model.Name + + if modelName != "Context" { + modelName = strings.TrimPrefix(modelName, "Context") + } + + modelName = strcase.ToSnake(modelName) + + modelProperty := &Property{ + Name: modelName, + Type: "model", + Description: model.Description, + } + + for _, field := range model.Fields { + tags, err := structtag.Parse(field.Tag) + if err != nil { + return b + } + + if !strings.Contains(field.Tag, "expr:") { + tags.Set(&structtag.Tag{Key: "expr", Name: strcase.ToSnake(field.Name)}) + } + + if !strings.Contains(field.Tag, "graphql:") { + tags.Set(&structtag.Tag{Key: "graphql", Name: strcase.ToLowerCamel(field.Name)}) + } + + exprTags, err := tags.Get("expr") + if err != nil { + panic(err) + } + + if exprTags.Name != "-" { + fieldType := field.Type.String() + + fieldProperty := &Property{ + Name: exprTags.Name, + Optional: field.Omittable || strings.HasPrefix(fieldType, "*"), + Description: field.Description, + } + + fieldProperty.IsSlice = strings.HasPrefix(fieldType, "[]") + + if strings.Contains(fieldType, "github.com/jippi/scm-engine") { + fieldType = filepath.Base(fieldType) + fieldType = strings.Split(fieldType, ".")[1] + fieldType = strings.TrimPrefix(fieldType, "Context") + fieldType = strcase.ToSnake(fieldType) + + fieldProperty.IsCustomType = true + } + + switch { + case strings.Contains(fieldType, "time.Time"): + fieldType = "time" + + case strings.Contains(fieldType, "time.Duration"): + fieldType = "duration" + } + + fieldProperty.Type = strings.TrimPrefix(fieldType, "*") + + modelProperty.AddAttribute(fieldProperty) + } + + slices.SortFunc(modelProperty.Attributes, sortSlice) + + field.Tag = tags.String() + } + + if strings.HasSuffix(model.Name, "Node") || model.Name == "Query" { + continue + } + + Props = append(Props, modelProperty) + PropMap[modelProperty.Name] = modelProperty + } + + return b +} + +// Property represents either a HCL block (with its sub-blocks or sub-attributes) +// or a single attribute (with no child nodes) +type Property struct { + // Name of the property (e.g. "merge_request") + Name string + Description string + + // Is the property optional? + Optional bool + + // The underlying type of the field (e.g. "string", "int", etc.) + Type string + + // Tracks if this property is a slice (wether its a list of blocks or a list of a scalar type). + // Used to show "String list" or "Block list" in the documentation output + IsSlice bool + + IsCustomType bool + + // Contains any sub-attributes for this Property. + Attributes []*Property + + // Used to track the hierarchy of properties - for example to compute the filename for external + // markdown documentation for the [Usage] field. + Parent *Property +} + +func (p *Property) AddAttribute(attrs ...*Property) { + for _, attr := range attrs { + if attr == nil { + return + } + + attr.Parent = p + + p.Attributes = append(p.Attributes, attr) + } +} + +// getHierarchy returns a slice representing all ancestors of this Property +// and its own Property name +func (p Property) getHierarchy() []string { + // This ensure the "root" node called [project] is not included in the hierarchy + if p.Parent == nil { + return nil + } + + out := []string{} + + if p.Parent != nil { + out = append(out, p.Parent.getHierarchy()...) + } + + name := p.Name + if p.IsSlice && p.IsCustomType { + name += "[]" + } + + return append(out, name) +} + +func (p *Property) BlockName() string { + if h := p.getHierarchy(); len(h) > 1 { + return strings.Join(h, ".") + } + + return p.Name +} + +func sortSlice(i, j *Property) int { + return cmp.Compare(i.Name, j.Name) +} diff --git a/schema/gitlab/gqlgen.yml b/schema/gitlab.gqlgen.yml similarity index 97% rename from schema/gitlab/gqlgen.yml rename to schema/gitlab.gqlgen.yml index b3d01aa..9d8d106 100644 --- a/schema/gitlab/gqlgen.yml +++ b/schema/gitlab.gqlgen.yml @@ -1,6 +1,6 @@ # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - - "*.graphqls" + - "gitlab.schema.graphqls" # Where should the generated server code go? exec: @@ -14,7 +14,7 @@ exec: # Where should any generated models go? model: - filename: ../../pkg/scm/gitlab/context.gen.go + filename: ../pkg/scm/gitlab/context.gen.go package: gitlab # Where should the resolver implementations go? diff --git a/schema/gitlab.schema.graphqls b/schema/gitlab.schema.graphqls new file mode 100644 index 0000000..42f5915 --- /dev/null +++ b/schema/gitlab.schema.graphqls @@ -0,0 +1,246 @@ +directive @generated on INPUT_FIELD_DEFINITION | FIELD_DEFINITION +directive @internal on INPUT_FIELD_DEFINITION | FIELD_DEFINITION +directive @expr(key: String!) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION +directive @graphql(key: String!) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + +scalar Time +scalar Duration + +type Context { + "The project the Merge Request belongs to" + Project: ContextProject @graphql(key: "project(fullPath: $project_id)") + + "The project group" + Group: ContextGroup @generated + + "Information about the Merge Request" + MergeRequest: ContextMergeRequest @generated +} + +type ContextProject { + # + # Native GraphQL fields - https://docs.gitlab.com/ee/api/graphql/reference/#project + # + + "Indicates the archived status of the project" + Archived: Boolean! + "Timestamp of the project creation" + CreatedAt: Time! + "Short description of the project" + Description: String! + "Full path of the project" + FullPath: String! + "ID of the project" + ID: String! + "Indicates if Issues are enabled for the current user" + IssuesEnabled: Boolean! + "Timestamp of the project last activity" + LastActivityAt: Time! + "Name of the project (without namespace)" + Name: String! + "Full name of the project with its namespace" + NameWithNamespace: String! + "Path of the project" + Path: String! + "List of project topics" + Topics: [String!] + "Visibility of the project" + Visibility: String! + + # + # Connections + # + + Labels: [ContextLabel!] @generated + ResponseLabels: ContextLabelNode @internal @graphql(key: "labels(first: 200)") + MergeRequest: ContextMergeRequest @internal @graphql(key: "mergeRequest(iid: $mr_id)") + ResponseGroup: ContextGroup @internal @graphql(key: "group") +} + +# https://docs.gitlab.com/ee/api/graphql/reference/#group +type ContextGroup { + "Description of the namespace" + Description: String! + "Indicates if a group has email notifications disabled" + EmailsDisabled: Boolean + "Indicates if a group has email notifications enabled" + EmailsEnabled: Boolean + "Full name of the namespace" + FullName: String! + "Full path of the namespace" + FullPath: String! + "ID of the namespace" + ID: String! + "Indicates if a group is disabled from getting mentioned" + MentionsDisabled: Boolean + "Name of the namespace" + Name: String! + "Path of the namespace" + Path: String! + "Visibility of the namespace" + Visibility: String + "Web URL of the group" + WebURL: String! +} + +# https://docs.gitlab.com/ee/api/graphql/reference/#mergerequest +type ContextMergeRequest { + "Number of approvals left" + ApprovalsLeft: Int + "Number of approvals required" + ApprovalsRequired: Int + "Indicates if the merge request has all the required approvals" + Approved: Boolean! + "Indicates if auto merge is enabled for the merge request" + AutoMergeEnabled: Boolean! + "Selected auto merge strategy" + AutoMergeStrategy: String + "Number of commits in the merge request" + CommitCount: Int + "Indicates if the merge request has conflicts" + Conflicts: Boolean! + "Timestamp of when the merge request was created" + CreatedAt: Time! + "Description of the merge request (Markdown rendered as HTML for caching)" + Description: String + "Indicates if comments on the merge request are locked to members only" + DiscussionLocked: Boolean! + "Indicates if the source branch is behind the target branch" + DivergedFromTargetBranch: Boolean! + "Indicates if the merge request is a draft" + Draft: Boolean! + "Indicates if the project settings will lead to source branch deletion after merge" + ForceRemoveSourceBranch: Boolean + "Number of downvotes for the merge request" + Downvotes: Int! + "ID of the merge request" + ID: String! + "Internal ID of the merge request" + IID: String! + "Indicates if the merge has been set to auto-merge" + MergeWhenPipelineSucceeds: Boolean + "Indicates if the merge request is mergeable" + Mergeable: Boolean! + "Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged" + MergeableDiscussionsState: Boolean + "Timestamp of when the merge request was merged, null if not merged" + MergedAt: Time + "Merge status of the merge request" + MergeStatusEnum: String! + "Timestamp of when the merge request was prepared" + PreparedAt: Time + "Indicates if the merge request will be rebased" + ShouldBeRebased: Boolean! + "Indicates if the source branch of the merge request will be deleted after merge" + ShouldRemoveSourceBranch: Boolean + "Source branch of the merge request" + SourceBranch: String! + "Indicates if the source branch of the merge request exists" + SourceBranchExists: Boolean! + "Indicates if the source branch is protected" + SourceBranchProtected: Boolean! + "Indicates if the merge request is set to be squashed when merged. Project settings may override this value. Use squash_on_merge instead to take project squash options into account" + Squash: Boolean! + "Indicates if the merge request will be squashed when merged" + SquashOnMerge: Boolean! + "State of the merge request" + State: String! + "Target branch of the merge request" + TargetBranch: String! + "Indicates if the target branch of the merge request exists" + TargetBranchExists: Boolean! + "Title of the merge request" + Title: String! + "Timestamp of when the merge request was last updated" + UpdatedAt: Time! + "Number of upvotes for the merge request." + Upvotes: Int! + "Number of user discussions in the merge request" + UserDiscussionsCount: Int + "User notes count of the merge request" + UserNotesCount: Int + + # + # Connections + # + + DiffStats: [ContextDiffStat!] + Labels: [ContextLabel!] @generated + ResponseLabels: ContextLabelNode @internal @graphql(key: "labels(first: 200)") + ResponseFirstCommits: ContextCommitsNode @internal @graphql(key: "first_commit: commits(first:1)") + ResponseLastCommits: ContextCommitsNode @internal @graphql(key: "last_commit: commits(last:1)") + + # + # scm-engine customs + # + + FirstCommit: ContextCommit @generated() + LastCommit: ContextCommit @generated() + TimeBetweenFirstAndLastCommit: Duration @generated() + TimeSinceFirstCommit: Duration @generated() + TimeSinceLastCommit: Duration @generated() +} + +# https://docs.gitlab.com/ee/api/graphql/reference/#commit +type ContextCommit { + "Commit author’s email" + AuthorEmail: String + "Commit authors name" + AuthorName: String + "Timestamp of when the commit was authored" + AuthoredDate: Time + "Timestamp of when the commit was committed" + CommittedDate: Time + "Email of the committer" + CommitterEmail: String + "Name of the committer" + CommitterName: String + "Description of the commit message" + Description: String + "Full title of the commit message" + FullTitle: String + "ID (global ID) of the commit" + ID: String + "Raw commit message" + Message: String + "SHA1 ID of the commit" + SHA: String! + "Short SHA1 ID of the commit" + ShortID: String! + "Title of the commit message" + Title: String + "Web URL of the commit" + WebURL: String! +} + +# Internal only, used to de-nest connections +type ContextCommitsNode { + Nodes: [ContextCommit!] @internal +} + +# https://docs.gitlab.com/ee/api/graphql/reference/#label +type ContextLabel { + "Background color of the label" + Color: String! + "Description of the label (Markdown rendered as HTML for caching)" + Description: String! + "Label ID" + ID: String! + "Content of the label" + Title: String! +} + +# Internal only, used to de-nest connections +type ContextLabelNode { + Nodes: [ContextLabel!] @internal +} + +# https://docs.gitlab.com/ee/api/graphql/reference/#diffstats +type ContextDiffStat { + "Number of lines added to this file" + Additions: Int! + "Number of lines deleted from this file" + Deletions: Int! + "File path, relative to repository root" + Path: String! +} diff --git a/schema/gitlab/generate.go b/schema/gitlab/generate.go deleted file mode 100644 index 45b1179..0000000 --- a/schema/gitlab/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package schema - -//go:generate go run main.go diff --git a/schema/gitlab/main.go b/schema/gitlab/main.go deleted file mode 100644 index e81240e..0000000 --- a/schema/gitlab/main.go +++ /dev/null @@ -1,111 +0,0 @@ -//go:build ignore - -package main - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "github.com/99designs/gqlgen/api" - "github.com/99designs/gqlgen/codegen/config" - "github.com/99designs/gqlgen/plugin/modelgen" - "github.com/fatih/structtag" - "github.com/iancoleman/strcase" - "github.com/vektah/gqlparser/v2/ast" -) - -// Defining mutation function -func constraintFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *modelgen.Field) (*modelgen.Field, error) { - // Call default hook to proceed standard directives like goField and goTag. - // You can omit it, if you don't need. - if f, err := modelgen.DefaultFieldMutateHook(td, fd, f); err != nil { - return f, err - } - - tags, err := structtag.Parse(f.Tag) - if err != nil { - return nil, err - } - - // Remove JSON tag, we don't need it - tags.Delete("json") - - if c := fd.Directives.ForName("internal"); c != nil { - tags.Set(&structtag.Tag{Key: "expr", Name: "-"}) - } else if c := fd.Directives.ForName("expr"); c != nil { - value := c.Arguments.ForName("key") - - if value != nil { - tags.Set(&structtag.Tag{Key: "expr", Name: value.Value.Raw}) - } - } - - if c := fd.Directives.ForName("generated"); c != nil { - tags.Set(&structtag.Tag{Key: "graphql", Name: "-"}) - } else if c := fd.Directives.ForName("graphql"); c != nil { - value := c.Arguments.ForName("key") - - if value != nil { - tags.Set(&structtag.Tag{Key: "graphql", Name: value.Value.Raw}) - } - } - - f.Tag = tags.String() - - return f, nil -} - -func mutateHook(b *modelgen.ModelBuild) *modelgen.ModelBuild { - for _, model := range b.Models { - for _, field := range model.Fields { - tags, err := structtag.Parse(field.Tag) - if err != nil { - return b - } - - if !strings.Contains(field.Tag, "expr:") { - tags.Set(&structtag.Tag{Key: "expr", Name: strcase.ToSnake(field.Name)}) - } - - if !strings.Contains(field.Tag, "graphql:") { - tags.Set(&structtag.Tag{Key: "graphql", Name: strcase.ToLowerCamel(field.Name)}) - } - - field.Tag = tags.String() - } - } - - return b -} - -func main() { - cfg, err := config.LoadConfig(getRootPath() + "/schema/gitlab/gqlgen.yml") - if err != nil { - fmt.Fprintln(os.Stderr, "failed to load config", err.Error()) - - os.Exit(2) - } - - // Attaching the mutation function onto modelgen plugin - p := modelgen.Plugin{ - FieldHook: constraintFieldHook, - MutateHook: mutateHook, - } - - err = api.Generate(cfg, api.ReplacePlugin(&p)) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(3) - } -} - -func getRootPath() string { - path, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() - if err != nil { - panic(err) - } - - return strings.TrimSpace(string(path)) -} diff --git a/schema/gitlab/schema.graphqls b/schema/gitlab/schema.graphqls deleted file mode 100644 index 67eb4e0..0000000 --- a/schema/gitlab/schema.graphqls +++ /dev/null @@ -1,110 +0,0 @@ -directive @generated on INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @internal on INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @expr(key: String!) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @graphql(key: String!) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION - -scalar Time -scalar Duration - -type Context { - Project: ContextProject @graphql(key: "project(fullPath: $project_id)") - Group: ContextGroup @generated - MergeRequest: ContextMergeRequest @generated -} - -type ContextProject { - ID: String! - Name: String! - NameWithNamespace: String! - Description: String! - Path: String! - FullPath: String! - Archived: Boolean! - Topics: [String!] - Visibility: String! - Labels: [ContextLabel!] @generated - LastActivityAt: Time! - CreatedAt: Time! - - # - # Internal state - # - - MergeRequest: ContextMergeRequest @internal @graphql(key: "mergeRequest(iid: $mr_id)") - ResponseLabels: ContextLabelNodes @internal @graphql(key: "labels(first: 200)") - ResponseGroup: ContextGroup @internal @graphql(key: "group") -} - -type ContextGroup { - ID: String! - Name: String! - Description: String! -} - -type ContextMergeRequest { - ApprovalsLeft: Int! - ApprovalsRequired: Int! - Approved: Boolean! - AutoMergeEnabled: Boolean! - AutoMergeStrategy: String! - Conflicts: Boolean! - CreatedAt: Time! - Description: String! - DiffStats: [ContextMergeRequestDiffStat!] - DivergedFromTargetBranch: Boolean! - Draft: Boolean! - FirstCommit: ContextCommit @generated() - ID: String! - IID: String! - Labels: [ContextLabel!] @generated - LastCommit: ContextCommit @generated() - Mergeable: Boolean! - MergedAt: Time - MergeStatusEnum: String! - SourceBranch: String! - SourceBranchExists: Boolean! - SourceBranchProtected: Boolean! - Squash: Boolean! - SquashOnMerge: Boolean! - State: String! - TargetBranch: String! - TargetBranchExists: Boolean! - TimeBetweenFirstAndLastCommit: Duration @generated() - TimeSinceFirstCommit: Duration @generated() - TimeSinceLastCommit: Duration @generated() - Title: String! - UpdatedAt: Time! - - # - # Internal State - # - ResponseLabels: ContextLabelNodes @internal @graphql(key: "labels(first: 200)") - ResponseFirstCommits: ContextCommitsNode @internal @graphql(key: "first_commit: commits(first:1)") - ResponseLastCommits: ContextCommitsNode @internal @graphql(key: "last_commit: commits(last:1)") -} - -type ContextCommit { - AuthorEmail: String! - CommittedDate: Time! -} - -type ContextCommitsNode { - Nodes: [ContextCommit!] @internal -} - -type ContextLabel { - ID: String! - Title: String! - Color: String! - Description: String! -} - -type ContextLabelNodes { - Nodes: [ContextLabel!] @internal -} - -type ContextMergeRequestDiffStat { - Path: String! - Additions: Int! - Deletions: Int! -}