| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "net/url" |
| "os" |
| "sort" |
| "sync" |
| |
| "fuchsia.googlesource.com/jiri" |
| "fuchsia.googlesource.com/jiri/cmdline" |
| "fuchsia.googlesource.com/jiri/gerrit" |
| "fuchsia.googlesource.com/jiri/log" |
| "fuchsia.googlesource.com/jiri/project" |
| ) |
| |
| var diffFlags struct { |
| cls bool |
| indentOutput bool |
| |
| // Need this to avoid infinite loop |
| maxCls uint |
| } |
| |
| var cmdDiff = &cmdline.Command{ |
| Runner: jiri.RunnerFunc(runDiff), |
| Name: "diff", |
| Short: "Prints diff between two snapshots", |
| ArgsName: "<snapshot-1> <snapshot-2>", |
| ArgsLong: "<snapshot-1/2> are files or urls containing snapshot", |
| Long: ` |
| Prints diff between two snapshots in json format. Max CLs returned for a |
| project is controlled by flag max-xls and is default by 5. The format of |
| returned json: |
| { |
| new_projects: [ |
| { |
| name: name, |
| path: path, |
| remote: remote, |
| revision: rev |
| },{...}... |
| ], |
| deleted_projects:[ |
| { |
| name: name, |
| path: path, |
| remote: remote, |
| revision: rev |
| },{...}... |
| ], |
| updated_projects:[ |
| { |
| name: name, |
| path: path, |
| remote: remote, |
| revision: rev |
| old_revision: old-rev, // if updated |
| old_path: old-path //if moved |
| cls:[ |
| { |
| number: num, |
| url: url, |
| commit: commit, |
| subject:sub |
| },{...},... |
| ] |
| has_more_cls: true, |
| error: error in retrieving CL |
| },{...}... |
| ] |
| } |
| `, |
| } |
| |
| func init() { |
| flags := &cmdDiff.Flags |
| flags.BoolVar(&diffFlags.cls, "cls", true, "Return CLs for changed projects") |
| flags.BoolVar(&diffFlags.indentOutput, "indent", true, "Indent json output") |
| flags.UintVar(&diffFlags.maxCls, "max-cls", 5, "Max number of CLs returned per changed project") |
| } |
| |
| type DiffCl struct { |
| Commit string `json:"commit"` |
| Number int `json:"number"` |
| Subject string `json:"subject"` |
| URL string `json:"url"` |
| } |
| |
| type DiffProject struct { |
| Name string `json:"name"` |
| Remote string `json:"remote"` |
| Path string `json:"path"` |
| OldPath string `json:"old_path,omitempty"` |
| Revision string `json:"revision"` |
| OldRevision string `json:"old_revision,omitempty"` |
| Cls []DiffCl `json:"cls,omitempty"` |
| Error string `json:"error,omitempty"` |
| HasMoreCls bool `json:"has_more_cls,omitempty"` |
| } |
| |
| type DiffProjectsByName []DiffProject |
| |
| func (p DiffProjectsByName) Len() int { |
| return len(p) |
| } |
| func (p DiffProjectsByName) Swap(i, j int) { |
| p[i], p[j] = p[j], p[i] |
| } |
| func (p DiffProjectsByName) Less(i, j int) bool { |
| return p[i].Name < p[j].Name |
| } |
| |
| type Diff struct { |
| NewProjects []DiffProject `json:"new_projects"` |
| DeletedProjects []DiffProject `json:"deleted_projects"` |
| UpdatedProjects []DiffProject `json:"updated_projects"` |
| } |
| |
| func (d *Diff) Sort() *Diff { |
| sort.Sort(DiffProjectsByName(d.NewProjects)) |
| sort.Sort(DiffProjectsByName(d.DeletedProjects)) |
| sort.Sort(DiffProjectsByName(d.UpdatedProjects)) |
| return d |
| } |
| |
| func runDiff(jirix *jiri.X, args []string) error { |
| if len(args) != 2 { |
| return jirix.UsageErrorf("Please provide two snapshots to diff") |
| } |
| d, err := getDiff(jirix, args[0], args[1]) |
| if err != nil { |
| return err |
| } |
| e := json.NewEncoder(os.Stdout) |
| if diffFlags.indentOutput { |
| e.SetIndent("", " ") |
| } |
| return e.Encode(d) |
| } |
| |
| func getDiff(jirix *jiri.X, snapshot1, snapshot2 string) (*Diff, error) { |
| diff := &Diff{ |
| NewProjects: make([]DiffProject, 0), |
| DeletedProjects: make([]DiffProject, 0), |
| UpdatedProjects: make([]DiffProject, 0), |
| } |
| oldLogger := jirix.Logger |
| defer func() { |
| jirix.Logger = oldLogger |
| }() |
| jirix.Logger = log.NewLogger(log.NoLogLevel, jirix.Color, false, 0, oldLogger.TimeLogThreshold(), nil, nil) |
| projects1, _, _, err := project.LoadSnapshotFile(jirix, snapshot1) |
| if err != nil { |
| return nil, err |
| } |
| projects2, _, _, err := project.LoadSnapshotFile(jirix, snapshot2) |
| if err != nil { |
| return nil, err |
| } |
| project.MatchLocalWithRemote(projects1, projects2) |
| jirix.Logger = oldLogger |
| |
| // Get deleted projects |
| for key, p1 := range projects1 { |
| if _, ok := projects2[key]; !ok { |
| diff.DeletedProjects = append(diff.DeletedProjects, DiffProject{ |
| Name: p1.Name, |
| Remote: p1.Remote, |
| Path: p1.Path, |
| Revision: p1.Revision, |
| }) |
| } |
| } |
| |
| // Get new projects and also extract updated projects |
| updatedProjectKeys := make(chan project.ProjectKey, len(projects2)) |
| for key, p2 := range projects2 { |
| if p1, ok := projects1[key]; !ok { |
| diff.NewProjects = append(diff.NewProjects, DiffProject{ |
| Name: p2.Name, |
| Remote: p2.Remote, |
| Path: p2.Path, |
| Revision: p2.Revision, |
| }) |
| } else { |
| if p1.Path != p2.Path || p1.Revision != p2.Revision { |
| updatedProjectKeys <- key |
| } |
| } |
| } |
| |
| close(updatedProjectKeys) |
| |
| processUpdatedProject := func(key project.ProjectKey) DiffProject { |
| p1 := projects1[key] |
| p2 := projects2[key] |
| diffP := DiffProject{ |
| Name: p2.Name, |
| Remote: p2.Remote, |
| Path: p2.Path, |
| Revision: p2.Revision, |
| } |
| if p1.Path != p2.Path { |
| diffP.OldPath = p1.Path |
| } |
| if p1.Revision != p2.Revision { |
| diffP.OldRevision = p1.Revision |
| if !diffFlags.cls { |
| // do nothing, prevents nested if/else |
| } else if p2.GerritHost == "" { |
| diffP.Error = "no gerrit host" |
| } else if hostUrl, err := url.Parse(p1.GerritHost); err != nil { |
| diffP.Error = fmt.Sprintf("invalid gerrit host %q: %s", p2.GerritHost, err) |
| } else { |
| g := gerrit.New(jirix, hostUrl) |
| revision := p2.Revision |
| for i := uint(0); i < diffFlags.maxCls && revision != p1.Revision; i++ { |
| cls, err := g.ListChangesByCommit(revision) |
| if err != nil { |
| diffP.Error = fmt.Sprintf("not able to get CL for revision %s: %s", revision, err) |
| break |
| } |
| var cl *gerrit.Change |
| for _, c := range cls { |
| if c.Current_revision == revision { |
| cl = &c |
| break |
| } |
| } |
| if cl == nil { |
| diffP.Error = fmt.Sprintf("not able to get CL for revision %s", revision) |
| break |
| } |
| diffCl := DiffCl{ |
| Commit: revision, |
| Number: cl.Number, |
| Subject: cl.Subject, |
| URL: fmt.Sprintf("%s/c/%d", p2.GerritHost, cl.Number), |
| } |
| diffP.Cls = append(diffP.Cls, diffCl) |
| parents := cl.Revisions[revision].Parents |
| if len(parents) != 1 { |
| if len(parents) == 0 { |
| diffP.Error = fmt.Sprintf("not able to get parent for revision %s", revision) |
| break |
| } else if len(parents) > 1 { |
| diffP.Error = fmt.Sprintf("more than one parent for revision %s", revision) |
| break |
| } |
| } |
| revision = parents[0].Commit |
| } |
| if revision != p1.Revision && diffP.Error == "" { |
| diffP.HasMoreCls = true |
| } |
| } |
| } |
| return diffP |
| } |
| |
| diffs := make(chan DiffProject, len(updatedProjectKeys)) |
| var wg sync.WaitGroup |
| for i := uint(0); i < jirix.Jobs; i++ { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| for key := range updatedProjectKeys { |
| diffs <- processUpdatedProject(key) |
| } |
| }() |
| } |
| wg.Wait() |
| close(diffs) |
| for diffP := range diffs { |
| diff.UpdatedProjects = append(diff.UpdatedProjects, diffP) |
| } |
| return diff.Sort(), nil |
| } |