go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/diff.go (about) 1 // Copyright 2017 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "net/url" 11 "os" 12 "path/filepath" 13 "sort" 14 "sync" 15 16 "go.fuchsia.dev/jiri" 17 "go.fuchsia.dev/jiri/cmdline" 18 "go.fuchsia.dev/jiri/gerrit" 19 "go.fuchsia.dev/jiri/log" 20 "go.fuchsia.dev/jiri/project" 21 ) 22 23 var diffFlags struct { 24 cls bool 25 indentOutput bool 26 27 // Need this to avoid infinite loop 28 maxCls uint 29 } 30 31 var cmdDiff = &cmdline.Command{ 32 Runner: jiri.RunnerFunc(runDiff), 33 Name: "diff", 34 Short: "Prints diff between two snapshots", 35 ArgsName: "<snapshot-1> <snapshot-2>", 36 ArgsLong: "<snapshot-1/2> are files or urls containing snapshot", 37 Long: ` 38 Prints diff between two snapshots in json format. Max CLs returned for a 39 project is controlled by flag max-xls and is default by 5. The format of 40 returned json: 41 { 42 new_projects: [ 43 { 44 name: name, 45 path: path, 46 relative_path: relative-path, 47 remote: remote, 48 revision: rev 49 },{...}... 50 ], 51 deleted_projects:[ 52 { 53 name: name, 54 path: path, 55 relative_path: relative-path, 56 remote: remote, 57 revision: rev 58 },{...}... 59 ], 60 updated_projects:[ 61 { 62 name: name, 63 path: path, 64 relative_path: relative-path, 65 remote: remote, 66 revision: rev 67 old_revision: old-rev, // if updated 68 old_path: old-path //if moved 69 old_relative_path: old-relative-path //if moved 70 cls:[ 71 { 72 number: num, 73 url: url, 74 commit: commit, 75 subject:sub 76 },{...},... 77 ] 78 has_more_cls: true, 79 error: error in retrieving CL 80 },{...}... 81 ] 82 } 83 `, 84 } 85 86 func init() { 87 flags := &cmdDiff.Flags 88 flags.BoolVar(&diffFlags.cls, "cls", true, "Return CLs for changed projects") 89 flags.BoolVar(&diffFlags.indentOutput, "indent", true, "Indent json output") 90 flags.UintVar(&diffFlags.maxCls, "max-cls", 5, "Max number of CLs returned per changed project") 91 } 92 93 type DiffCl struct { 94 Commit string `json:"commit"` 95 Number int `json:"number"` 96 Subject string `json:"subject"` 97 URL string `json:"url"` 98 } 99 100 type DiffProject struct { 101 Name string `json:"name"` 102 Remote string `json:"remote"` 103 Path string `json:"path"` 104 RelativePath string `json:"relative_path"` 105 OldPath string `json:"old_path,omitempty"` 106 OldRelativePath string `json:"old_relative_path,omitempty"` 107 Revision string `json:"revision"` 108 OldRevision string `json:"old_revision,omitempty"` 109 Cls []DiffCl `json:"cls,omitempty"` 110 Error string `json:"error,omitempty"` 111 HasMoreCls bool `json:"has_more_cls,omitempty"` 112 } 113 114 type DiffProjectsByName []DiffProject 115 116 func (p DiffProjectsByName) Len() int { 117 return len(p) 118 } 119 func (p DiffProjectsByName) Swap(i, j int) { 120 p[i], p[j] = p[j], p[i] 121 } 122 func (p DiffProjectsByName) Less(i, j int) bool { 123 return p[i].Name < p[j].Name 124 } 125 126 type Diff struct { 127 NewProjects []DiffProject `json:"new_projects"` 128 DeletedProjects []DiffProject `json:"deleted_projects"` 129 UpdatedProjects []DiffProject `json:"updated_projects"` 130 } 131 132 func (d *Diff) Sort() *Diff { 133 sort.Sort(DiffProjectsByName(d.NewProjects)) 134 sort.Sort(DiffProjectsByName(d.DeletedProjects)) 135 sort.Sort(DiffProjectsByName(d.UpdatedProjects)) 136 return d 137 } 138 139 func runDiff(jirix *jiri.X, args []string) error { 140 if len(args) != 2 { 141 return jirix.UsageErrorf("Please provide two snapshots to diff") 142 } 143 d, err := getDiff(jirix, args[0], args[1]) 144 if err != nil { 145 return err 146 } 147 e := json.NewEncoder(os.Stdout) 148 if diffFlags.indentOutput { 149 e.SetIndent("", " ") 150 } 151 return e.Encode(d) 152 } 153 154 func getDiff(jirix *jiri.X, snapshot1, snapshot2 string) (*Diff, error) { 155 diff := &Diff{ 156 NewProjects: make([]DiffProject, 0), 157 DeletedProjects: make([]DiffProject, 0), 158 UpdatedProjects: make([]DiffProject, 0), 159 } 160 oldLogger := jirix.Logger 161 defer func() { 162 jirix.Logger = oldLogger 163 }() 164 jirix.Logger = log.NewLogger(log.NoLogLevel, jirix.Color, false, 0, oldLogger.TimeLogThreshold(), nil, nil) 165 projects1, _, _, err := project.LoadSnapshotFile(jirix, snapshot1) 166 if err != nil { 167 return nil, err 168 } 169 projects2, _, _, err := project.LoadSnapshotFile(jirix, snapshot2) 170 if err != nil { 171 return nil, err 172 } 173 project.MatchLocalWithRemote(projects1, projects2) 174 jirix.Logger = oldLogger 175 176 // Get deleted projects 177 for key, p1 := range projects1 { 178 if _, ok := projects2[key]; !ok { 179 rp, err := filepath.Rel(jirix.Root, p1.Path) 180 if err != nil { 181 // should not happen 182 panic(err) 183 } 184 diff.DeletedProjects = append(diff.DeletedProjects, DiffProject{ 185 Name: p1.Name, 186 Remote: p1.Remote, 187 Path: p1.Path, 188 RelativePath: rp, 189 Revision: p1.Revision, 190 }) 191 } 192 } 193 194 // Get new projects and also extract updated projects 195 updatedProjectKeys := make(chan project.ProjectKey, len(projects2)) 196 for key, p2 := range projects2 { 197 if p1, ok := projects1[key]; !ok { 198 rp, err := filepath.Rel(jirix.Root, p2.Path) 199 if err != nil { 200 // should not happen 201 panic(err) 202 } 203 204 diff.NewProjects = append(diff.NewProjects, DiffProject{ 205 Name: p2.Name, 206 Remote: p2.Remote, 207 Path: p2.Path, 208 RelativePath: rp, 209 Revision: p2.Revision, 210 }) 211 } else { 212 if p1.Path != p2.Path || p1.Revision != p2.Revision { 213 updatedProjectKeys <- key 214 } 215 } 216 } 217 218 close(updatedProjectKeys) 219 220 processUpdatedProject := func(key project.ProjectKey) DiffProject { 221 p1 := projects1[key] 222 p2 := projects2[key] 223 rp, err := filepath.Rel(jirix.Root, p2.Path) 224 if err != nil { 225 // should not happen 226 panic(err) 227 } 228 diffP := DiffProject{ 229 Name: p2.Name, 230 Remote: p2.Remote, 231 Path: p2.Path, 232 RelativePath: rp, 233 Revision: p2.Revision, 234 } 235 if p1.Path != p2.Path { 236 rp, err := filepath.Rel(jirix.Root, p1.Path) 237 if err != nil { 238 // should not happen 239 panic(err) 240 } 241 diffP.OldPath = p1.Path 242 diffP.OldRelativePath = rp 243 } 244 if p1.Revision != p2.Revision { 245 diffP.OldRevision = p1.Revision 246 if !diffFlags.cls { 247 // do nothing, prevents nested if/else 248 } else if p2.GerritHost == "" { 249 diffP.Error = "no gerrit host" 250 } else if hostUrl, err := url.Parse(p1.GerritHost); err != nil { 251 diffP.Error = fmt.Sprintf("invalid gerrit host %q: %s", p2.GerritHost, err) 252 } else { 253 g := gerrit.New(jirix, hostUrl) 254 revision := p2.Revision 255 for i := uint(0); i < diffFlags.maxCls && revision != p1.Revision; i++ { 256 cls, err := g.ListChangesByCommit(revision) 257 if err != nil { 258 diffP.Error = fmt.Sprintf("not able to get CL for revision %s: %s", revision, err) 259 break 260 } 261 var cl *gerrit.Change 262 for _, c := range cls { 263 if c.Current_revision == revision { 264 cl = &c 265 break 266 } 267 } 268 if cl == nil { 269 diffP.Error = fmt.Sprintf("not able to get CL for revision %s", revision) 270 break 271 } 272 diffCl := DiffCl{ 273 Commit: revision, 274 Number: cl.Number, 275 Subject: cl.Subject, 276 URL: fmt.Sprintf("%s/c/%d", p2.GerritHost, cl.Number), 277 } 278 diffP.Cls = append(diffP.Cls, diffCl) 279 parents := cl.Revisions[revision].Parents 280 if len(parents) != 1 { 281 if len(parents) == 0 { 282 diffP.Error = fmt.Sprintf("not able to get parent for revision %s", revision) 283 break 284 } else if len(parents) > 1 { 285 diffP.Error = fmt.Sprintf("more than one parent for revision %s", revision) 286 break 287 } 288 } 289 revision = parents[0].Commit 290 } 291 if revision != p1.Revision && diffP.Error == "" { 292 diffP.HasMoreCls = true 293 } 294 } 295 } 296 return diffP 297 } 298 299 diffs := make(chan DiffProject, len(updatedProjectKeys)) 300 var wg sync.WaitGroup 301 for i := uint(0); i < jirix.Jobs; i++ { 302 wg.Add(1) 303 go func() { 304 defer wg.Done() 305 for key := range updatedProjectKeys { 306 diffs <- processUpdatedProject(key) 307 } 308 }() 309 } 310 wg.Wait() 311 close(diffs) 312 for diffP := range diffs { 313 diff.UpdatedProjects = append(diff.UpdatedProjects, diffP) 314 } 315 return diff.Sort(), nil 316 }