mirror of https://github.com/grafana/grafana.git
[Prototyping] Add repository history endpoint (#97345)
This commit is contained in:
parent
d6e7edbd1e
commit
a722b485da
|
|
@ -82,6 +82,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
|||
&WebhookResponse{},
|
||||
&ResourceWrapper{},
|
||||
&FileList{},
|
||||
&HistoryList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,3 +260,27 @@ type FileItem struct {
|
|||
Modified int64 `json:"modified,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// HistoryList is a list of versions of a resource
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type HistoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// should be named "items", but avoid subresource error for now:
|
||||
// kubernetes/kubernetes#126809
|
||||
Items []HistoryItem `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatarURL,omitempty"`
|
||||
}
|
||||
|
||||
type HistoryItem struct {
|
||||
Ref string `json:"ref"`
|
||||
Message string `json:"message"`
|
||||
Authors []Author `json:"authors"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,22 @@ import (
|
|||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Author) DeepCopyInto(out *Author) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Author.
|
||||
func (in *Author) DeepCopy() *Author {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Author)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EditingOptions) DeepCopyInto(out *EditingOptions) {
|
||||
*out = *in
|
||||
|
|
@ -115,6 +131,60 @@ func (in *HelloWorld) DeepCopyObject() runtime.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HistoryItem) DeepCopyInto(out *HistoryItem) {
|
||||
*out = *in
|
||||
if in.Authors != nil {
|
||||
in, out := &in.Authors, &out.Authors
|
||||
*out = make([]Author, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoryItem.
|
||||
func (in *HistoryItem) DeepCopy() *HistoryItem {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HistoryItem)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HistoryList) DeepCopyInto(out *HistoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]HistoryItem, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoryList.
|
||||
func (in *HistoryList) DeepCopy() *HistoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HistoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *HistoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LintIssue) DeepCopyInto(out *LintIssue) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ import (
|
|||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.EditingOptions": schema_pkg_apis_provisioning_v0alpha1_EditingOptions(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HelloWorld": schema_pkg_apis_provisioning_v0alpha1_HelloWorld(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem": schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryList": schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LintIssue": schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Repository": schema_pkg_apis_provisioning_v0alpha1_Repository(ref),
|
||||
|
|
@ -33,6 +36,39 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_Author(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"username": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"avatarURL": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "username"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_EditingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
@ -270,6 +306,104 @@ func schema_pkg_apis_provisioning_v0alpha1_HelloWorld(ref common.ReferenceCallba
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"ref": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"message": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"authors": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"createdAt": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"ref", "message", "authors", "createdAt"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "HistoryList is a list of versions of a resource",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "should be named \"items\", but avoid subresource error for now: kubernetes/kubernetes#126809",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
@ -534,9 +668,9 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref common.ReferenceC
|
|||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
|
||||
},
|
||||
},
|
||||
"store": {
|
||||
"existing": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The value with the same name that is currently saved",
|
||||
Description: "The same value, currently saved in the grafana database",
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,HistoryItem,Authors
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,Errors
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,Lint
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,FileList,Items
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,GitHub
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,PreferYAML
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceObjects,Existing
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
type historySubresource struct {
|
||||
repoGetter RepoGetter
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (h *historySubresource) New() runtime.Object {
|
||||
// This is added as the "ResponseType" regardless what ProducesObject() returns
|
||||
return &provisioning.HistoryList{}
|
||||
}
|
||||
|
||||
func (h *historySubresource) Destroy() {}
|
||||
|
||||
func (h *historySubresource) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *historySubresource) GetSingularName() string {
|
||||
return "History"
|
||||
}
|
||||
|
||||
func (h *historySubresource) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"}
|
||||
}
|
||||
|
||||
func (h *historySubresource) ProducesObject(verb string) runtime.Object {
|
||||
return &provisioning.HistoryList{}
|
||||
}
|
||||
|
||||
func (h *historySubresource) ConnectMethods() []string {
|
||||
return []string{http.MethodGet}
|
||||
}
|
||||
|
||||
func (h *historySubresource) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, true, "" // true adds the {path} component
|
||||
}
|
||||
|
||||
func (h *historySubresource) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
logger := h.logger.With("repository_name", name)
|
||||
repo, err := h.repoGetter.GetRepository(ctx, name)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, "failed to find repository", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
ref := query.Get("ref")
|
||||
logger = logger.With("ref", ref)
|
||||
ctx := r.Context()
|
||||
|
||||
var filePath string
|
||||
prefix := fmt.Sprintf("/%s/history/", name)
|
||||
idx := strings.Index(r.URL.Path, prefix)
|
||||
if idx != -1 {
|
||||
filePath = r.URL.Path[idx+len(prefix):]
|
||||
}
|
||||
|
||||
logger = logger.With("path", filePath)
|
||||
|
||||
commits, err := repo.History(ctx, logger, filePath, ref)
|
||||
if err != nil {
|
||||
logger.DebugContext(ctx, "failed to get history", "error", err)
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
responder.Object(http.StatusOK, &provisioning.HistoryList{Items: commits})
|
||||
}), nil
|
||||
}
|
||||
|
|
@ -198,6 +198,10 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
|
|||
client: b.client,
|
||||
logger: b.logger.With("connector", "files"),
|
||||
}
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("history")] = &historySubresource{
|
||||
repoGetter: b,
|
||||
logger: b.logger.With("connector", "history"),
|
||||
}
|
||||
storage[provisioning.RepositoryResourceInfo.StoragePath("import")] = &importConnector{
|
||||
repoGetter: b,
|
||||
client: b.client,
|
||||
|
|
@ -562,6 +566,18 @@ func (b *ProvisioningAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.
|
|||
sub.Post.Parameters = []*spec3.Parameter{ref}
|
||||
}
|
||||
|
||||
sub = oas.Paths.Paths[repoprefix+"/history"]
|
||||
if sub != nil {
|
||||
sub.Get.Description = "Get the history of the repository"
|
||||
sub.Get.Parameters = []*spec3.Parameter{ref}
|
||||
}
|
||||
|
||||
sub = oas.Paths.Paths[repoprefix+"/history/{path}"]
|
||||
if sub != nil {
|
||||
sub.Get.Description = "Get the history of a path"
|
||||
sub.Get.Parameters = []*spec3.Parameter{ref}
|
||||
}
|
||||
|
||||
// Show a special list command
|
||||
sub = oas.Paths.Paths[repoprefix+"/files"]
|
||||
if sub != nil {
|
||||
|
|
|
|||
|
|
@ -264,6 +264,55 @@ func (r *githubRepository) Delete(ctx context.Context, logger *slog.Logger, path
|
|||
return r.gh.DeleteFile(ctx, owner, repo, path, ref, comment, file.GetSHA())
|
||||
}
|
||||
|
||||
func (r *githubRepository) History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error) {
|
||||
if ref == "" {
|
||||
ref = r.config.Spec.GitHub.Branch
|
||||
}
|
||||
|
||||
commits, err := r.gh.Commits(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, path, ref)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgh.ErrResourceNotFound) {
|
||||
return nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "path not found",
|
||||
Code: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("get commits: %w", err)
|
||||
}
|
||||
|
||||
ret := make([]provisioning.HistoryItem, 0, len(commits))
|
||||
for _, commit := range commits {
|
||||
authors := make([]provisioning.Author, 0)
|
||||
if commit.Author != nil {
|
||||
authors = append(authors, provisioning.Author{
|
||||
Name: commit.Author.Name,
|
||||
Username: commit.Author.Username,
|
||||
AvatarURL: commit.Author.AvatarURL,
|
||||
})
|
||||
}
|
||||
|
||||
if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name {
|
||||
authors = append(authors, provisioning.Author{
|
||||
Name: commit.Committer.Name,
|
||||
Username: commit.Committer.Username,
|
||||
AvatarURL: commit.Committer.AvatarURL,
|
||||
})
|
||||
}
|
||||
|
||||
ret = append(ret, provisioning.HistoryItem{
|
||||
Ref: commit.Ref,
|
||||
Message: commit.Message,
|
||||
Authors: authors,
|
||||
CreatedAt: commit.CreatedAt.UnixNano(),
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// basicGitBranchNameRegex is a regular expression to validate a git branch name
|
||||
// it does not cover all cases as positive lookaheads are not supported in Go's regexp
|
||||
var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package github
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v66/github"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
|
@ -66,6 +67,9 @@ type Client interface {
|
|||
// If ".." appears in the "path", this method will return an error.
|
||||
DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error
|
||||
|
||||
// Commits returns the commits for the given path
|
||||
Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error)
|
||||
|
||||
// CreateBranch creates a new branch in the repository.
|
||||
CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error
|
||||
// BranchExists checks if a branch exists in the repository.
|
||||
|
|
@ -101,6 +105,20 @@ type RepositoryContent interface {
|
|||
GetSize() int64
|
||||
}
|
||||
|
||||
type CommitAuthor struct {
|
||||
Name string
|
||||
Username string
|
||||
AvatarURL string
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Ref string
|
||||
Message string
|
||||
Author *CommitAuthor
|
||||
Committer *CommitAuthor
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type CommitFile interface {
|
||||
GetSHA() string
|
||||
GetFilename() string
|
||||
|
|
|
|||
|
|
@ -59,6 +59,36 @@ func (_m *MockClient) ClearAllPullRequestFileComments(ctx context.Context, owner
|
|||
return r0
|
||||
}
|
||||
|
||||
// Commits provides a mock function with given fields: ctx, owner, repository, path, branch
|
||||
func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) {
|
||||
ret := _m.Called(ctx, owner, repository, path, branch)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Commits")
|
||||
}
|
||||
|
||||
var r0 []Commit
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]Commit, error)); ok {
|
||||
return rf(ctx, owner, repository, path, branch)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []Commit); ok {
|
||||
r0 = rf(ctx, owner, repository, path, branch)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]Commit)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok {
|
||||
r1 = rf(ctx, owner, repository, path, branch)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName
|
||||
func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error {
|
||||
ret := _m.Called(ctx, owner, repository, sourceBranch, branchName)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v66/github"
|
||||
)
|
||||
|
|
@ -183,6 +184,62 @@ func (r *realImpl) DeleteFile(ctx context.Context, owner, repository, path, bran
|
|||
return err
|
||||
}
|
||||
|
||||
func (r *realImpl) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) {
|
||||
commits, _, err := r.gh.Repositories.ListCommits(ctx, owner, repository, &github.CommitsListOptions{
|
||||
Path: path,
|
||||
SHA: branch,
|
||||
})
|
||||
if err != nil {
|
||||
var ghErr *github.ErrorResponse
|
||||
if !errors.As(err, &ghErr) {
|
||||
return nil, err
|
||||
}
|
||||
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
||||
return nil, ErrServiceUnavailable
|
||||
}
|
||||
|
||||
if ghErr.Response.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]Commit, 0, len(commits))
|
||||
for _, c := range commits {
|
||||
var createdAt time.Time
|
||||
var author *CommitAuthor
|
||||
if c.GetCommit().GetAuthor() != nil {
|
||||
author = &CommitAuthor{
|
||||
Name: c.GetCommit().GetAuthor().GetName(),
|
||||
Username: c.GetAuthor().GetLogin(),
|
||||
AvatarURL: c.GetAuthor().GetAvatarURL(),
|
||||
}
|
||||
|
||||
createdAt = c.GetCommit().GetAuthor().GetDate().Time
|
||||
}
|
||||
|
||||
var committer *CommitAuthor
|
||||
if c.GetCommitter() != nil {
|
||||
committer = &CommitAuthor{
|
||||
Name: c.GetCommit().GetCommitter().GetName(),
|
||||
Username: c.GetCommitter().GetLogin(),
|
||||
AvatarURL: c.GetCommitter().GetAvatarURL(),
|
||||
}
|
||||
}
|
||||
|
||||
ret = append(ret, Commit{
|
||||
Ref: c.GetSHA(),
|
||||
Message: c.GetCommit().GetMessage(),
|
||||
Author: author,
|
||||
Committer: committer,
|
||||
CreatedAt: createdAt,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *realImpl) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error {
|
||||
// Fail if the branch already exists
|
||||
if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil {
|
||||
|
|
|
|||
|
|
@ -267,6 +267,15 @@ func (r *localRepository) Delete(ctx context.Context, logger *slog.Logger, path
|
|||
return os.Remove(filepath.Join(r.path, path))
|
||||
}
|
||||
|
||||
func (r *localRepository) History(ctx context.Context, logger *slog.Logger, path string, ref string) ([]provisioning.HistoryItem, error) {
|
||||
return nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "history is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *localRepository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ type Repository interface {
|
|||
// Delete a file in the remote repository
|
||||
Delete(ctx context.Context, logger *slog.Logger, path, ref, message string) error
|
||||
|
||||
// History of changes for a path
|
||||
History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error)
|
||||
|
||||
// For repositories that support webhooks
|
||||
Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc
|
||||
// Hooks called after the repository has been created, updated or deleted
|
||||
|
|
|
|||
|
|
@ -99,6 +99,15 @@ func (r *s3Repository) Delete(ctx context.Context, logger *slog.Logger, path str
|
|||
}
|
||||
}
|
||||
|
||||
func (r *s3Repository) History(ctx context.Context, logger *slog.Logger, path string, ref string) ([]provisioning.HistoryItem, error) {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "history is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *s3Repository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ func (r *unknownRepository) Delete(ctx context.Context, logger *slog.Logger, pat
|
|||
}
|
||||
}
|
||||
|
||||
func (r *unknownRepository) History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error) {
|
||||
return nil, &errors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "history is not yet implemented",
|
||||
Code: http.StatusNotImplemented,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook implements provisioning.Repository.
|
||||
func (r *unknownRepository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
|
||||
// webhooks are not supported with local
|
||||
|
|
|
|||
|
|
@ -121,6 +121,15 @@ func TestIntegrationProvisioning(t *testing.T) {
|
|||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "repositories/history",
|
||||
"singularName": "",
|
||||
"namespaced": true,
|
||||
"kind": "HistoryList",
|
||||
"verbs": [
|
||||
"get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "repositories/import",
|
||||
"singularName": "",
|
||||
|
|
|
|||
Loading…
Reference in New Issue