1package main
2
3import (
4	"encoding/json"
5	"errors"
6	"flag"
7	"fmt"
8	"io/ioutil"
9	"os"
10	"path/filepath"
11	"strconv"
12
13	"github.com/aws/aws-sdk-go-v2/internal/repotools/changes"
14)
15
16var changeParams = struct {
17	module      string
18	changeType  changes.ChangeType
19	description string
20	compareTo   string
21	similar     bool
22}{}
23
24var addFlags *flag.FlagSet
25var lsFlags *flag.FlagSet
26var modifyFlags *flag.FlagSet
27var rmFlags *flag.FlagSet
28
29func changeUsage() {
30	sets := []*flag.FlagSet{addFlags, lsFlags, modifyFlags, rmFlags}
31
32	for _, f := range sets {
33		f.Usage()
34	}
35}
36
37func init() {
38	addFlags = flag.NewFlagSet("add", flag.ExitOnError)
39	addFlags.StringVar(&changeParams.module, "module", "", "sets the change's module")
40	addFlags.Var(&changeParams.changeType, "type", "sets the change's type")
41	addFlags.StringVar(&changeParams.description, "description", "", "sets the change's description")
42	addFlags.StringVar(&changeParams.compareTo, "compare-to", "", "specifies a path to a version enclosure to compare current module hashes to in order to resolve a wildcard.")
43	addFlags.Usage = func() {
44		fmt.Printf("%s change add [-module=<module>] [-type=<type>] [-description=<description>]\n", os.Args[0])
45		addFlags.PrintDefaults()
46	}
47
48	lsFlags = flag.NewFlagSet("ls", flag.ExitOnError)
49	lsFlags.StringVar(&changeParams.module, "module", "", "filters changes by module")
50	lsFlags.Usage = func() {
51		fmt.Printf("%s change ls [-module=<module>]\n", os.Args[0])
52		lsFlags.PrintDefaults()
53	}
54
55	modifyFlags = flag.NewFlagSet("modify", flag.ExitOnError)
56	modifyFlags.Usage = func() {
57		fmt.Printf("%s change modify <change id>\n  <change id>: the index (as found in the ls subcommand) or the ID of the change to modify\n", os.Args[0])
58		modifyFlags.PrintDefaults()
59	}
60
61	rmFlags = flag.NewFlagSet("rm", flag.ExitOnError)
62	rmFlags.Usage = func() {
63		fmt.Printf("%s change rm <change id>\n  <change id>: the index (as found in the ls subcommand) or the ID of the change to remove\n", os.Args[0])
64		rmFlags.PrintDefaults()
65	}
66}
67
68func changeSubcmd(args []string) error {
69	if len(args) == 0 {
70		changeUsage()
71		return errors.New("invalid usage")
72	}
73
74	subCommand := args[0]
75
76	changesPath, err := changes.GetChangesPath()
77	if err != nil {
78		return fmt.Errorf("failed to load .changes directory: %v", err)
79	}
80
81	metadata, err := changes.LoadMetadata(changesPath)
82	if err != nil {
83		return fmt.Errorf("failed to load .changes directory: %v", err)
84	}
85
86	switch subCommand {
87	case "add", "new":
88		err = addFlags.Parse(args[1:])
89		if err != nil {
90			return err
91		}
92
93		if changes.ModIsWildcard(changeParams.module) {
94			return addCmdWildcard(metadata, changeParams.module, changeParams.changeType, changeParams.description, changeParams.compareTo)
95		}
96
97		return addCmd(metadata, changeParams.module, changeParams.changeType, changeParams.description)
98	case "ls", "list":
99		err = lsFlags.Parse(args[1:])
100		if err != nil {
101			return err
102		}
103
104		return lsCmd(metadata, changeParams.module)
105	case "modify", "edit":
106		err = modifyFlags.Parse(args[1:])
107		if err != nil {
108			return err
109		}
110
111		if len(args) < 2 {
112			changeUsage()
113			return errors.New("invalid usage")
114		}
115
116		id := args[1]
117
118		return modifyCmd(metadata, id)
119	case "rm", "delete":
120		err = rmFlags.Parse(args[1:])
121		if err != nil {
122			return err
123		}
124
125		if len(args) < 2 {
126			changeUsage()
127			return errors.New("invalid usage")
128		}
129
130		id := args[1]
131
132		return rmCmd(metadata, id)
133	default:
134		changeUsage()
135		return errors.New("invalid usage")
136	}
137}
138
139func addCmdWildcard(metadata *changes.Metadata, module string, changeType changes.ChangeType, description string, compareTo string) error {
140	if module == "" {
141		return errors.New("couldn't add wildcard change: a module must be provided with --module")
142	}
143
144	repo, err := changes.NewRepository(filepath.Join(metadata.ChangePath, ".."))
145	if err != nil {
146		return fmt.Errorf("couldn't add wildcard change: %v", err)
147	}
148
149	mods, err := repo.Modules()
150	if err != nil {
151		return err
152	}
153
154	var affectedModules []string
155	if compareTo != "" {
156		data, err := ioutil.ReadFile(compareTo)
157		if err != nil {
158			return err
159		}
160
161		var enc changes.VersionEnclosure
162		err = json.Unmarshal(data, &enc)
163		if err != nil {
164			return err
165		}
166
167		hashes, err := repo.ModuleHashes(enc)
168		if err != nil {
169			return err
170		}
171
172		affectedModules = enc.HashDiff(hashes)
173	} else {
174		affectedModules, err = changes.MatchWildcardModules(mods, module)
175		if err != nil {
176			return err
177		}
178	}
179
180	template, err := changes.ChangeToTemplate(changes.Change{
181		Module:          module,
182		Type:            changeType,
183		Description:     description,
184		AffectedModules: affectedModules,
185	})
186	if err != nil {
187		return fmt.Errorf("failed to create change: %v", err)
188	}
189
190	filledTemplate, err := editTemplate(template)
191	if err != nil {
192		return fmt.Errorf("failed to create change: %v", err)
193	}
194
195	changes, err := changes.TemplateToChanges(filledTemplate)
196	if err != nil {
197		return fmt.Errorf("failed to create change: %v", err)
198	}
199
200	if len(changes) != 1 {
201		return fmt.Errorf("failed to create change: expected template to create 1 change, got %d changes", len(changes))
202	}
203
204	change := changes[0]
205
206	// TODO: move some logic into TemplateToChanges
207
208	return metadata.AddChange(change)
209}
210
211func addCmd(metadata *changes.Metadata, module string, changeType changes.ChangeType, description string) error {
212	if module == "" {
213		currentModule, err := changes.GetCurrentModule()
214		if err != nil {
215			return fmt.Errorf("failed to create change: the module flag was not provided and the tool could not detect a module")
216		}
217
218		module = currentModule
219	}
220
221	var newChanges []changes.Change
222	var err error
223
224	if changeType != "" && description != "" {
225		newChanges, err = changes.NewChanges([]string{module}, changeType, description, "")
226		if err != nil {
227			return fmt.Errorf("failed to create change: %v", err)
228		}
229
230		err = metadata.AddChanges(newChanges)
231		if err != nil {
232			return fmt.Errorf("failed to create change: %v", err)
233		}
234	} else {
235		template, err := changes.ChangeToTemplate(changes.Change{
236			Module: module,
237		})
238		if err != nil {
239			return fmt.Errorf("failed to create change: %v", err)
240		}
241
242		filledTemplate, err := editTemplate(template)
243		if err != nil {
244			return fmt.Errorf("failed to create change: %v", err)
245		}
246
247		newChanges, err = metadata.AddChangesFromTemplate(filledTemplate)
248		if err != nil {
249			return fmt.Errorf("failed to create change: %v", err)
250		}
251	}
252
253	for _, c := range newChanges {
254		fmt.Println("added change with id " + c.ID)
255	}
256
257	return nil
258}
259
260func lsCmd(metadata *changes.Metadata, module string) error {
261	for i, c := range metadata.Changes {
262		if c.Module == module || module == "" {
263			fmt.Printf("[%d] %s\n", i, c.ID)
264			fmt.Println("\t", c.Type)
265			fmt.Println("\t", c.Description)
266			fmt.Println()
267		}
268	}
269
270	return nil
271}
272
273func modifyCmd(metadata *changes.Metadata, id string) error {
274	change, err := selectChange(metadata, id)
275	if err != nil {
276		return fmt.Errorf("failed to modify change: %v", err)
277	}
278
279	template, err := changes.ChangeToTemplate(change)
280	if err != nil {
281		return fmt.Errorf("failed to modify change: %v", err)
282	}
283
284	filledTemplate, err := editTemplate(template)
285	if err != nil {
286		return fmt.Errorf("failed to modify change: %v", err)
287	}
288
289	newChanges, err := metadata.UpdateChangeFromTemplate(change, filledTemplate)
290	if err != nil {
291		return fmt.Errorf("couldn't modify change: %v", err)
292	}
293
294	fmt.Printf("successfully modified %s, new change(s):\n", change.ID)
295	for _, c := range newChanges {
296		fmt.Printf("\t%s\n", c.ID)
297	}
298	return nil
299}
300
301func rmCmd(metadata *changes.Metadata, id string) error {
302	change, err := selectChange(metadata, id)
303	if err != nil {
304		return fmt.Errorf("failed to remove change: %v", err)
305	}
306
307	err = metadata.RemoveChangeByID(change.ID)
308	if err != nil {
309		return fmt.Errorf("failed to remove change: %v", err)
310	}
311
312	fmt.Println("successfully removed " + change.ID)
313	return nil
314}
315
316// selectChange will return the change identified by the given id, which can be either the index of one of metadata's
317// Changes or the Change's ID.
318func selectChange(metadata *changes.Metadata, id string) (changes.Change, error) {
319	// try selecting by index first
320	index, err := strconv.Atoi(id)
321	if err == nil {
322		if index < 0 || index >= len(metadata.Changes) {
323			return changes.Change{}, fmt.Errorf("failed to get change with index %d: index out of range", index)
324		}
325		return metadata.Changes[index], nil
326	}
327
328	return metadata.GetChangeByID(id)
329}
330