-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
project.go
190 lines (161 loc) · 6.35 KB
/
project.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package config
import (
"fmt"
"net/http"
"os"
"github.com/xanzy/go-gitlab"
"gitlab.com/tozd/go/errors"
)
// getProject populates configuration struct with configuration available
// from GitLab projects API endpoint.
func (c *GetCommand) getProject(client *gitlab.Client, configuration *Configuration) (bool, errors.E) {
fmt.Fprintf(os.Stderr, "Getting project...\n")
descriptions, errE := getProjectDescriptions(c.DocsRef)
if errE != nil {
return false, errE
}
u := fmt.Sprintf("projects/%s", gitlab.PathEscape(c.Project))
req, err := client.NewRequest(http.MethodGet, u, nil, nil)
if err != nil {
return false, errors.WithMessage(err, `failed to get project`)
}
project := map[string]interface{}{}
_, err = client.Do(req, &project)
if err != nil {
return false, errors.WithMessage(err, `failed to get project`)
}
hasSensitive := false
// We use a separate top-level configuration for avatar instead.
s, errE := c.getAvatar(client, project, configuration)
if errE != nil {
return false, errE
}
hasSensitive = hasSensitive || s
// We use a separate top-level configuration for shared with groups instead.
s, errE = c.getSharedWithGroups(client, project, configuration)
if errE != nil {
return false, errE
}
hasSensitive = hasSensitive || s
// We use a separate top-level configuration for fork relationship.
s, errE = c.getForkedFromProject(client, project, configuration)
if errE != nil {
return false, errE
}
hasSensitive = hasSensitive || s
// Only retain those keys which can be edited through the API
// (which are those available in descriptions). We cannot add comments
// at the same time because we might delete them, too, because they are
// not found in descriptions.
for key := range project {
_, ok := descriptions[key]
if !ok {
delete(project, key)
}
}
// This cannot be configured simply through the edit API, this just enabled/disables it.
// We use a separate top-level configuration for mirroring instead.
delete(project, "mirror")
// Remove deprecated name_regex key in favor of new name_regex_delete.
if project["container_expiration_policy"] != nil {
policy, ok := project["container_expiration_policy"].(map[string]interface{})
if !ok {
return false, errors.New(`invalid "container_expiration_policy"`)
}
if policy["name_regex"] != nil && policy["name_regex_delete"] == nil {
policy["name_regex_delete"] = policy["name_regex"]
delete(policy, "name_regex")
} else if policy["name_regex"] != nil && policy["name_regex_delete"] != nil {
delete(policy, "name_regex")
}
// It is not an editable key.
delete(policy, "next_run_at")
}
// Add comments for keys. We process these keys before writing YAML out.
describeKeys(project, descriptions)
configuration.Project = project
return hasSensitive, nil
}
// parseProjectDocumentation parses GitLab's documentation in Markdown for
// projects API endpoint and extracts description of fields used to describe
// an individual project.
func parseProjectDocumentation(input []byte) (map[string]string, errors.E) {
return parseTable(input, "Edit project", func(key string) string {
switch key {
case "public_builds":
// "public_jobs" is used in get,
// while "public_builds" is used in edit.
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/329725
return "public_jobs"
case "container_expiration_policy_attributes":
// "container_expiration_policy" is used in get,
// while "container_expiration_policy_attributes" is used in edit.
return "container_expiration_policy"
case "show_default_award_emojis":
// Currently it does not work.
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/348365
return ""
case "name", "visibility":
// Only owners can have "name" and "visibility" fields present in edit
// project API request, otherwise GitLab returns 403, but we want it
// to work for maintainers as well. One can include these fields
// manually into project configuration and it will work for owners.
return ""
case "path":
// If "path" is included in the request, the request does not
// do anything, even for the owner.
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/348635
return ""
default:
return key
}
})
}
// getProjectDescriptions obtains description of fields used to describe
// an individual project from GitLab's documentation for projects API endpoint.
func getProjectDescriptions(gitRef string) (map[string]string, errors.E) {
data, err := downloadFile(fmt.Sprintf("https://gitlab.com/gitlab-org/gitlab/-/raw/%s/doc/api/projects.md", gitRef))
if err != nil {
return nil, errors.WithMessage(err, "failed to get project configuration descriptions")
}
return parseProjectDocumentation(data)
}
// updateProject updates GitLab project's configuration using GitLab projects API endpoint
// based on the configuration struct.
func (c *SetCommand) updateProject(client *gitlab.Client, configuration *Configuration) errors.E {
if configuration.Project == nil {
return nil
}
fmt.Fprintf(os.Stderr, "Updating project...\n")
u := fmt.Sprintf("projects/%s", gitlab.PathEscape(c.Project))
// For now we provide both keys, the new and the deprecated.
containerExpirationPolicy, ok := configuration.Project["container_expiration_policy"]
if ok {
if containerExpirationPolicy != nil {
containerExpirationPolicy, ok := containerExpirationPolicy.(map[string]interface{}) //nolint:govet
if !ok {
return errors.New(`invalid "container_expiration_policy"`)
}
containerExpirationPolicy["name_regex"] = containerExpirationPolicy["name_regex_delete"]
configuration.Project["container_expiration_policy"] = containerExpirationPolicy
}
// We have to rename the key to what is used in edit.
configuration.Project["container_expiration_policy_attributes"] = configuration.Project["container_expiration_policy"]
delete(configuration.Project, "container_expiration_policy")
}
// We have to rename the key to what is used in edit.
publicJobs, ok := configuration.Project["public_jobs"]
if ok {
configuration.Project["public_builds"] = publicJobs
delete(configuration.Project, "public_jobs")
}
req, err := client.NewRequest(http.MethodPut, u, configuration.Project, nil)
if err != nil {
return errors.WithMessage(err, "failed to update GitLab project")
}
_, err = client.Do(req, nil)
if err != nil {
return errors.WithMessage(err, "failed to update GitLab project")
}
return nil
}