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!
-}