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  }