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