Repository: xanzy/go-gitlab
Pull request #1573 (2022-11-13 15:25:28)

Implement Metadata API

timofurrer: @svanharmelen Can you help me get this released?

Patch:
diff --git a/gitlab.go b/gitlab.go
index 50e084c..d9001eb 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -156,6 +156,7 @@ type Client struct {
 	Markdown                *MarkdownService
 	MergeRequestApprovals   *MergeRequestApprovalsService
 	MergeRequests           *MergeRequestsService
+	Metadata                *MetadataService
 	Milestones              *MilestonesService
 	Namespaces              *NamespacesService
 	Notes                   *NotesService
@@ -361,6 +362,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Markdown = &MarkdownService{client: c}
 	c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c}
 	c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats}
+	c.Metadata = &MetadataService{client: c}
 	c.Milestones = &MilestonesService{client: c}
 	c.Namespaces = &NamespacesService{client: c}
 	c.Notes = &NotesService{client: c}
diff --git a/groups.go b/groups.go
index 2f3de16..96a1e3a 100644
--- a/groups.go
+++ b/groups.go
@@ -18,13 +18,10 @@ package gitlab
 
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"time"
-
-	retryablehttp "github.com/hashicorp/go-retryablehttp"
 )
 
 // GroupsService handles communication with the group related methods of
@@ -94,15 +91,6 @@ type GroupAvatar struct {
 	Image    io.Reader
 }
 
-// MarshalJSON implements the json.Marshaler interface.
-func (a *GroupAvatar) MarshalJSON() ([]byte, error) {
-	if a.Filename == "" && a.Image == nil {
-		return []byte(`""`), nil
-	}
-	type alias GroupAvatar
-	return json.Marshal((*alias)(a))
-}
-
 // LDAPGroupLink represents a GitLab LDAP group link.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#ldap-group-links
@@ -333,7 +321,6 @@ func (s *GroupsService) DownloadAvatar(gid interface{}, options ...RequestOption
 type CreateGroupOptions struct {
 	Name                           *string                     `url:"name,omitempty" json:"name,omitempty"`
 	Path                           *string                     `url:"path,omitempty" json:"path,omitempty"`
-	Avatar                         *GroupAvatar                `url:"-" json:"-"`
 	Description                    *string                     `url:"description,omitempty" json:"description,omitempty"`
 	MembershipLock                 *bool                       `url:"membership_lock,omitempty" json:"membership_lock,omitempty"`
 	Visibility                     *VisibilityValue            `url:"visibility,omitempty" json:"visibility,omitempty"`
@@ -358,22 +345,7 @@ type CreateGroupOptions struct {
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group
 func (s *GroupsService) CreateGroup(opt *CreateGroupOptions, options ...RequestOptionFunc) (*Group, *Response, error) {
-	var err error
-	var req *retryablehttp.Request
-
-	if opt.Avatar == nil {
-		req, err = s.client.NewRequest(http.MethodPost, "groups", opt, options)
-	} else {
-		req, err = s.client.UploadRequest(
-			http.MethodPost,
-			"groups",
-			opt.Avatar.Image,
-			opt.Avatar.Filename,
-			UploadAvatar,
-			opt,
-			options,
-		)
-	}
+	req, err := s.client.NewRequest(http.MethodPost, "groups", opt, options)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -457,7 +429,6 @@ func (s *GroupsService) TransferSubGroup(gid interface{}, opt *TransferSubGroupO
 type UpdateGroupOptions struct {
 	Name                                 *string                     `url:"name,omitempty" json:"name,omitempty"`
 	Path                                 *string                     `url:"path,omitempty" json:"path,omitempty"`
-	Avatar                               *GroupAvatar                `url:"-" json:"avatar,omitempty"`
 	Description                          *string                     `url:"description,omitempty" json:"description,omitempty"`
 	MembershipLock                       *bool                       `url:"membership_lock,omitempty" json:"membership_lock,omitempty"`
 	Visibility                           *VisibilityValue            `url:"visibility,omitempty" json:"visibility,omitempty"`
@@ -491,21 +462,7 @@ func (s *GroupsService) UpdateGroup(gid interface{}, opt *UpdateGroupOptions, op
 	}
 	u := fmt.Sprintf("groups/%s", PathEscape(group))
 
-	var req *retryablehttp.Request
-
-	if opt.Avatar == nil || (opt.Avatar.Filename == "" && opt.Avatar.Image == nil) {
-		req, err = s.client.NewRequest(http.MethodPut, u, opt, options)
-	} else {
-		req, err = s.client.UploadRequest(
-			http.MethodPut,
-			u,
-			opt.Avatar.Image,
-			opt.Avatar.Filename,
-			UploadAvatar,
-			opt,
-			options,
-		)
-	}
+	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/metadata.go b/metadata.go
new file mode 100644
index 0000000..29436fd
--- /dev/null
+++ b/metadata.go
@@ -0,0 +1,63 @@
+//
+// Copyright 2022, Timo Furrer <tuxtimo@gmail.com>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import "net/http"
+
+// MetadataService handles communication with the GitLab server instance to
+// retrieve its metadata information via the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/version.md
+type MetadataService struct {
+	client *Client
+}
+
+// Metadata represents a GitLab instance version.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/metadata.md
+type Metadata struct {
+	Version  string `json:"version"`
+	Revision string `json:"revision"`
+	KAS      struct {
+		Enabled     bool   `json:"enabled"`
+		ExternalURL string `json:"externalUrl"`
+		Version     string `json:"version"`
+	} `json:"kas"`
+}
+
+func (s Metadata) String() string {
+	return Stringify(s)
+}
+
+// GetMetadata gets a GitLab server instance meteadata.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/metadata.md
+func (s *MetadataService) GetMetadata(options ...RequestOptionFunc) (*Metadata, *Response, error) {
+	req, err := s.client.NewRequest(http.MethodGet, "metadata", nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	v := new(Metadata)
+	resp, err := s.client.Do(req, v)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return v, resp, err
+}
+
diff --git a/project_feature_flags.go b/project_feature_flags.go
index 79aaf8b..c01a17e 100644
--- a/project_feature_flags.go
+++ b/project_feature_flags.go
@@ -137,7 +137,6 @@ type CreateProjectFeatureFlagOptions struct {
 // Gitlab API docs:
 // https://docs.gitlab.com/ee/api/feature_flags.html#create-a-feature-flag
 type FeatureFlagStrategyOptions struct {
-	ID         *int                                 `url:"id,omitempty" json:"id,omitempty"`
 	Name       *string                              `url:"name,omitempty" json:"name,omitempty"`
 	Parameters *ProjectFeatureFlagStrategyParameter `url:"parameters,omitempty" json:"parameters,omitempty"`
 	Scopes     *[]*ProjectFeatureFlagScope          `url:"scopes,omitempty" json:"scopes,omitempty"`
diff --git a/projects.go b/projects.go
index 676a4b7..6afc4c6 100644
--- a/projects.go
+++ b/projects.go
@@ -17,7 +17,6 @@
 package gitlab
 
 import (
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -121,7 +120,6 @@ type Project struct {
 	SharedWithGroups                          []struct {
 		GroupID          int    `json:"group_id"`
 		GroupName        string `json:"group_name"`
-		GroupFullPath    string `json:"group_full_path"`
 		GroupAccessLevel int    `json:"group_access_level"`
 	} `json:"shared_with_groups"`
 	Statistics                               *Statistics        `json:"statistics"`
@@ -712,15 +710,6 @@ type ProjectAvatar struct {
 	Image    io.Reader
 }
 
-// MarshalJSON implements the json.Marshaler interface.
-func (a *ProjectAvatar) MarshalJSON() ([]byte, error) {
-	if a.Filename == "" && a.Image == nil {
-		return []byte(`""`), nil
-	}
-	type alias ProjectAvatar
-	return json.Marshal((*alias)(a))
-}
-
 // CreateProject creates a new project owned by the authenticated user.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project
@@ -822,7 +811,7 @@ type EditProjectOptions struct {
 	AutoDevopsDeployStrategy                  *string                              `url:"auto_devops_deploy_strategy,omitempty" json:"auto_devops_deploy_strategy,omitempty"`
 	AutoDevopsEnabled                         *bool                                `url:"auto_devops_enabled,omitempty" json:"auto_devops_enabled,omitempty"`
 	AutocloseReferencedIssues                 *bool                                `url:"autoclose_referenced_issues,omitempty" json:"autoclose_referenced_issues,omitempty"`
-	Avatar                                    *ProjectAvatar                       `url:"-" json:"avatar,omitempty"`
+	Avatar                                    *ProjectAvatar                       `url:"-" json:"-"`
 	BuildCoverageRegex                        *string                              `url:"build_coverage_regex,omitempty" json:"build_coverage_regex,omitempty"`
 	BuildGitStrategy                          *string                              `url:"build_git_strategy,omitempty" json:"build_git_strategy,omitempty"`
 	BuildTimeout                              *int                                 `url:"build_timeout,omitempty" json:"build_timeout,omitempty"`
@@ -912,11 +901,11 @@ func (s *ProjectsService) EditProject(pid interface{}, opt *EditProjectOptions,
 
 	var req *retryablehttp.Request
 
-	if opt.Avatar == nil || (opt.Avatar.Filename == "" && opt.Avatar.Image == nil) {
+	if opt.Avatar == nil {
 		req, err = s.client.NewRequest(http.MethodPut, u, opt, options)
 	} else {
 		req, err = s.client.UploadRequest(
-			http.MethodPut,
+			http.MethodPost,
 			u,
 			opt.Avatar.Image,
 			opt.Avatar.Filename,
diff --git a/settings.go b/settings.go
index 5905307..6ac05ae 100644
--- a/settings.go
+++ b/settings.go
@@ -64,7 +64,6 @@ type Settings struct {
 	AutoDevOpsDomain                                      string            `json:"auto_devops_domain"`
 	AutoDevOpsEnabled                                     bool              `json:"auto_devops_enabled"`
 	AutomaticPurchasedStorageAllocation                   bool              `json:"automatic_purchased_storage_allocation"`
-	CanCreateGroup                                        bool              `json:"can_create_group"`
 	CheckNamespacePlan                                    bool              `json:"check_namespace_plan"`
 	CommitEmailHostname                                   string            `json:"commit_email_hostname"`
 	ContainerExpirationPoliciesEnableHistoricEntries      bool              `json:"container_expiration_policies_enable_historic_entries"`
@@ -432,7 +431,6 @@ type UpdateSettingsOptions struct {
 	AutoDevOpsDomain                                      *string            `url:"auto_devops_domain,omitempty" json:"auto_devops_domain,omitempty"`
 	AutoDevOpsEnabled                                     *bool              `url:"auto_devops_enabled,omitempty" json:"auto_devops_enabled,omitempty"`
 	AutomaticPurchasedStorageAllocation                   *bool              `url:"automatic_purchased_storage_allocation,omitempty" json:"automatic_purchased_storage_allocation,omitempty"`
-	CanCreateGroup                                        *bool              `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
 	CheckNamespacePlan                                    *bool              `url:"check_namespace_plan,omitempty" json:"check_namespace_plan,omitempty"`
 	CommitEmailHostname                                   *string            `url:"commit_email_hostname,omitempty" json:"commit_email_hostname,omitempty"`
 	ContainerExpirationPoliciesEnableHistoricEntries      *bool              `url:"container_expiration_policies_enable_historic_entries,omitempty" json:"container_expiration_policies_enable_historic_entries,omitempty"`
diff --git a/users.go b/users.go
index 61973a9..f04acfb 100644
--- a/users.go
+++ b/users.go
@@ -745,9 +745,8 @@ func (s *UsersService) DeleteGPGKeyForUser(user, key int, options ...RequestOpti
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-emails
 type Email struct {
-	ID          int        `json:"id"`
-	Email       string     `json:"email"`
-	ConfirmedAt *time.Time `json:"confirmed_at"`
+	ID    int    `json:"id"`
+	Email string `json:"email"`
 }
 
 // ListEmails gets a list of currently authenticated user's Emails.


Test patch:
diff --git a/metadata_test.go b/metadata_test.go
new file mode 100644
index 0000000..cbdbbd5
--- /dev/null
+++ b/metadata_test.go
@@ -0,0 +1,54 @@
+//
+// Copyright 2022, Timo Furrer <tuxtimo@gmail.com>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestGetMetadata(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/metadata",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodGet)
+			fmt.Fprint(w, `{"version":"15.6.0-pre","revision":"016e8d8bdc3","kas":{"enabled":true,"externalUrl":"wss://kas.gitlab.com","version":"15.6.0-rc2"}}`)
+		})
+
+	version, _, err := client.Metadata.GetMetadata()
+	if err != nil {
+		t.Errorf("Metadata.GetMetadata returned error: %v", err)
+	}
+
+	want := &Metadata{Version: "15.6.0-pre", Revision: "016e8d8bdc3", KAS: struct {
+		Enabled     bool   `json:"enabled"`
+		ExternalURL string `json:"externalUrl"`
+		Version     string `json:"version"`
+	}{
+		Enabled:     true,
+		ExternalURL: "wss://kas.gitlab.com",
+		Version:     "15.6.0-rc2",
+	}}
+	if !reflect.DeepEqual(want, version) {
+		t.Errorf("Metadata.GetMetadata returned %+v, want %+v", version, want)
+	}
+}
+