1/*
2 * Copyright (c) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package gitee_common
17
18import (
19	"bufio"
20	"bytes"
21	"encoding/xml"
22	"fmt"
23	"fotff/vcs"
24	"fotff/vcs/gitee"
25	"github.com/huandu/go-clone"
26	"github.com/sirupsen/logrus"
27	"os"
28	"path/filepath"
29	"regexp"
30	"sort"
31	"strconv"
32	"strings"
33	"sync"
34	"time"
35)
36
37type IssueInfo struct {
38	visited          bool
39	RelatedIssues    []string
40	MRs              []*gitee.Commit
41	StructCTime      string
42	StructureUpdates []*vcs.ProjectUpdate
43}
44
45type Step struct {
46	IssueURLs        []string
47	MRs              []*gitee.Commit
48	StructCTime      string
49	StructureUpdates []*vcs.ProjectUpdate
50}
51
52func (m *Manager) stepsFromGitee(from, to string) (pkgs []string, err error) {
53	updates, err := m.getRepoUpdates(from, to)
54	if err != nil {
55		return nil, err
56	}
57	startTime, err := parseTime(from)
58	if err != nil {
59		return nil, err
60	}
61	endTime, err := parseTime(to)
62	if err != nil {
63		return nil, err
64	}
65	logrus.Infof("find %d repo updates from %s to %s", len(updates), from, to)
66	steps, err := getAllStepsFromGitee(startTime, endTime, m.Branch, m.ManifestBranch, updates)
67	if err != nil {
68		return nil, err
69	}
70	logrus.Infof("find total %d steps from %s to %s", len(steps), from, to)
71	baseManifest, err := vcs.ParseManifestFile(filepath.Join(m.Workspace, from, "manifest_tag.xml"))
72	if err != nil {
73		return nil, err
74	}
75	for _, step := range steps {
76		var newPkg string
77		if newPkg, baseManifest, err = m.genStepPackage(baseManifest, step); err != nil {
78			return nil, err
79		}
80		pkgs = append(pkgs, newPkg)
81	}
82	return pkgs, nil
83}
84
85func (m *Manager) getRepoUpdates(from, to string) (updates []vcs.ProjectUpdate, err error) {
86	m1, err := vcs.ParseManifestFile(filepath.Join(m.Workspace, from, "manifest_tag.xml"))
87	if err != nil {
88		return nil, err
89	}
90	m2, err := vcs.ParseManifestFile(filepath.Join(m.Workspace, to, "manifest_tag.xml"))
91	if err != nil {
92		return nil, err
93	}
94	return vcs.GetRepoUpdates(m1, m2)
95}
96
97func getAllStepsFromGitee(startTime, endTime time.Time, branch string, manifestBranch string, updates []vcs.ProjectUpdate) (ret []Step, err error) {
98	allMRs, err := getAllMRs(startTime, endTime, branch, manifestBranch, updates)
99	if err != nil {
100		return nil, err
101	}
102	issueInfos, err := combineMRsToIssue(allMRs, branch)
103	if err != nil {
104		return nil, err
105	}
106	return combineIssuesToStep(issueInfos)
107}
108
109func getAllMRs(startTime, endTime time.Time, branch string, manifestBranch string, updates []vcs.ProjectUpdate) (allMRs []*gitee.Commit, err error) {
110	var once sync.Once
111	for _, update := range updates {
112		var prs []*gitee.Commit
113		if update.P1.StructureDiff(update.P2) {
114			once.Do(func() {
115				prs, err = gitee.GetBetweenTimeMRs("openharmony", "manifest", manifestBranch, startTime, endTime)
116			})
117			if update.P1 != nil {
118				var p1 []*gitee.Commit
119				p1, err = gitee.GetBetweenTimeMRs("openharmony", update.P1.Name, branch, startTime, endTime)
120				prs = append(prs, p1...)
121			}
122			if update.P2 != nil {
123				var p2 []*gitee.Commit
124				p2, err = gitee.GetBetweenTimeMRs("openharmony", update.P2.Name, branch, startTime, endTime)
125				prs = append(prs, p2...)
126			}
127		} else {
128			prs, err = gitee.GetBetweenMRs(gitee.CompareParam{
129				Head:  update.P2.Revision,
130				Base:  update.P1.Revision,
131				Owner: "openharmony",
132				Repo:  update.P2.Name,
133			})
134		}
135		if err != nil {
136			return nil, err
137		}
138		allMRs = append(allMRs, prs...)
139	}
140	logrus.Infof("find total %d merge request commits of all repo updates", len(allMRs))
141	return
142}
143
144func combineMRsToIssue(allMRs []*gitee.Commit, branch string) (map[string]*IssueInfo, error) {
145	ret := make(map[string]*IssueInfo)
146	for _, mr := range allMRs {
147		num, err := strconv.Atoi(strings.Trim(regexp.MustCompile(`!\d+ `).FindString(mr.Commit.Message), "! "))
148		if err != nil {
149			return nil, fmt.Errorf("parse MR message for %s fail: %s", mr.URL, err)
150		}
151		issues, err := gitee.GetMRIssueURL(mr.Owner, mr.Repo, num)
152		if err != nil {
153			return nil, err
154		}
155		if len(issues) == 0 {
156			issues = []string{mr.URL}
157		}
158		var scs []*vcs.ProjectUpdate
159		var scTime string
160		if mr.Owner == "openharmony" && mr.Repo == "manifest" {
161			if scTime, scs, err = parseStructureUpdates(mr, branch); err != nil {
162				return nil, err
163			}
164		}
165		for i, issue := range issues {
166			if _, ok := ret[issue]; !ok {
167				ret[issue] = &IssueInfo{
168					MRs:              []*gitee.Commit{mr},
169					RelatedIssues:    append(issues[:i], issues[i+1:]...),
170					StructCTime:      scTime,
171					StructureUpdates: scs,
172				}
173			} else {
174				ret[issue] = &IssueInfo{
175					MRs:              append(ret[issue].MRs, mr),
176					RelatedIssues:    append(ret[issue].RelatedIssues, append(issues[:i], issues[i+1:]...)...),
177					StructCTime:      scTime,
178					StructureUpdates: append(ret[issue].StructureUpdates, scs...),
179				}
180			}
181		}
182	}
183	logrus.Infof("find total %d issues of all repo updates", len(ret))
184	return ret, nil
185}
186
187func combineOtherRelatedIssue(parent, self *IssueInfo, all map[string]*IssueInfo) {
188	if self.visited {
189		return
190	}
191	self.visited = true
192	for _, other := range self.RelatedIssues {
193		if son, ok := all[other]; ok {
194			combineOtherRelatedIssue(self, son, all)
195			delete(all, other)
196		}
197	}
198	parent.RelatedIssues = deDupIssues(append(parent.RelatedIssues, self.RelatedIssues...))
199	parent.MRs = deDupMRs(append(parent.MRs, self.MRs...))
200	parent.StructureUpdates = deDupProjectUpdates(append(parent.StructureUpdates, self.StructureUpdates...))
201	if len(parent.StructCTime) != 0 && parent.StructCTime < self.StructCTime {
202		parent.StructCTime = self.StructCTime
203	}
204}
205
206func deDupProjectUpdates(us []*vcs.ProjectUpdate) (retMRs []*vcs.ProjectUpdate) {
207	dupIndexes := make([]bool, len(us))
208	for i := range us {
209		for j := i + 1; j < len(us); j++ {
210			if us[j].P1 == us[i].P1 && us[j].P2 == us[i].P2 {
211				dupIndexes[j] = true
212			}
213		}
214	}
215	for i, dup := range dupIndexes {
216		if dup {
217			continue
218		}
219		retMRs = append(retMRs, us[i])
220	}
221	return
222}
223
224func deDupMRs(mrs []*gitee.Commit) (retMRs []*gitee.Commit) {
225	tmp := make(map[string]*gitee.Commit)
226	for _, m := range mrs {
227		tmp[m.SHA] = m
228	}
229	for _, m := range tmp {
230		retMRs = append(retMRs, m)
231	}
232	return
233}
234
235func deDupIssues(issues []string) (retIssues []string) {
236	tmp := make(map[string]string)
237	for _, i := range issues {
238		tmp[i] = i
239	}
240	for _, i := range tmp {
241		retIssues = append(retIssues, i)
242	}
243	return
244}
245
246// parseStructureUpdates get changed XMLs and parse it to recognize repo structure changes.
247// Since we do not care which revision a repo was, P1 is not welly handled, just assign it not nil for performance.
248func parseStructureUpdates(commit *gitee.Commit, branch string) (string, []*vcs.ProjectUpdate, error) {
249	tmp := make(map[string]vcs.ProjectUpdate)
250	if len(commit.Files) == 0 {
251		// commit that queried from MR req does not contain file details, should fetch again
252		var err error
253		if commit, err = gitee.GetCommit(commit.Owner, commit.Repo, commit.SHA); err != nil {
254			return "", nil, err
255		}
256	}
257	for _, f := range commit.Files {
258		if filepath.Ext(f.Filename) != ".xml" {
259			continue
260		}
261		if err := parseFilePatch(f.Patch, tmp); err != nil {
262			return "", nil, err
263		}
264	}
265	var ret []*vcs.ProjectUpdate
266	for _, pu := range tmp {
267		projectUpdateCopy := pu
268		ret = append(ret, &projectUpdateCopy)
269	}
270	for _, pu := range ret {
271		if pu.P1 == nil && pu.P2 != nil {
272			lastCommit, err := gitee.GetLatestMRBefore("openharmony", pu.P2.Name, branch, commit.Commit.Committer.Date)
273			if err != nil {
274				return "", nil, err
275			}
276			pu.P2.Revision = lastCommit.SHA
277		}
278	}
279	return commit.Commit.Committer.Date, ret, nil
280}
281
282func parseFilePatch(str string, m map[string]vcs.ProjectUpdate) error {
283	sc := bufio.NewScanner(bytes.NewBuffer([]byte(str)))
284	for sc.Scan() {
285		line := sc.Text()
286		var p vcs.Project
287		if strings.HasPrefix(line, "-") {
288			if err := xml.Unmarshal([]byte(line[1:]), &p); err == nil {
289				m[p.Name] = vcs.ProjectUpdate{P1: &p, P2: m[p.Name].P2}
290			}
291		} else if strings.HasPrefix(line, "+") {
292			if err := xml.Unmarshal([]byte(line[1:]), &p); err == nil {
293				m[p.Name] = vcs.ProjectUpdate{P1: m[p.Name].P1, P2: &p}
294			}
295		}
296	}
297	return nil
298}
299
300func combineIssuesToStep(issueInfos map[string]*IssueInfo) (ret []Step, err error) {
301	for _, info := range issueInfos {
302		combineOtherRelatedIssue(info, info, issueInfos)
303	}
304	for issue, infos := range issueInfos {
305		sort.Slice(infos.MRs, func(i, j int) bool {
306			// move the latest MR to the first place, use its merged_time to represent the update time of the issue
307			return infos.MRs[i].Commit.Committer.Date > infos.MRs[j].Commit.Committer.Date
308		})
309		ret = append(ret, Step{
310			IssueURLs:        append(infos.RelatedIssues, issue),
311			MRs:              infos.MRs,
312			StructCTime:      infos.StructCTime,
313			StructureUpdates: infos.StructureUpdates})
314	}
315	sort.Slice(ret, func(i, j int) bool {
316		ti, tj := ret[i].MRs[0].Commit.Committer.Date, ret[j].MRs[0].Commit.Committer.Date
317		if len(ret[i].StructCTime) != 0 {
318			ti = ret[i].StructCTime
319		}
320		if len(ret[j].StructCTime) != 0 {
321			ti = ret[j].StructCTime
322		}
323		return ti < tj
324	})
325	logrus.Infof("find total %d steps of all issues", len(ret))
326	return
327}
328
329func parseTime(pkg string) (time.Time, error) {
330	t, err := time.ParseInLocation(`20060102_150405`, regexp.MustCompile(`\d{8}_\d{6}`).FindString(pkg), time.Local)
331	if err != nil {
332		return time.ParseInLocation(`20060102150405`, regexp.MustCompile(`\d{14}`).FindString(pkg), time.Local)
333	}
334	return t, nil
335}
336
337func (m *Manager) genStepPackage(base *vcs.Manifest, step Step) (newPkg string, newManifest *vcs.Manifest, err error) {
338	defer func() {
339		logrus.Infof("package dir %s for step %v generated", newPkg, step.IssueURLs)
340	}()
341	newManifest = clone.Clone(base).(*vcs.Manifest)
342	for _, u := range step.StructureUpdates {
343		if u.P2 != nil {
344			newManifest.UpdateManifestProject(u.P2.Name, u.P2.Path, u.P2.Remote, u.P2.Revision, true)
345		} else if u.P1 != nil {
346			newManifest.RemoveManifestProject(u.P1.Name)
347		}
348	}
349	for _, mr := range step.MRs {
350		newManifest.UpdateManifestProject(mr.Repo, "", "", mr.SHA, false)
351	}
352	md5sum, err := newManifest.Standardize()
353	if err != nil {
354		return "", nil, err
355	}
356	if err := os.MkdirAll(filepath.Join(m.Workspace, md5sum), 0750); err != nil {
357		return "", nil, err
358	}
359	if err := os.WriteFile(filepath.Join(m.Workspace, md5sum, "__last_issue__"), []byte(fmt.Sprintf("%v", step.IssueURLs)), 0640); err != nil {
360		return "", nil, err
361	}
362	err = newManifest.WriteFile(filepath.Join(m.Workspace, md5sum, "manifest_tag.xml"))
363	if err != nil {
364		return "", nil, err
365	}
366	return md5sum, newManifest, nil
367}
368