github.com/git-lfs/git-lfs@v2.5.2+incompatible/commands/command_migrate_info.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/git-lfs/git-lfs/errors"
    12  	"github.com/git-lfs/git-lfs/git/githistory"
    13  	"github.com/git-lfs/git-lfs/tasklog"
    14  	"github.com/git-lfs/git-lfs/tools"
    15  	"github.com/git-lfs/git-lfs/tools/humanize"
    16  	"github.com/git-lfs/gitobj"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  var (
    21  	// migrateInfoTopN is a flag given to the git-lfs-migrate(1) subcommand
    22  	// 'info' which specifies how many info entries to show by default.
    23  	migrateInfoTopN int
    24  
    25  	// migrateInfoAboveFmt is a flag given to the git-lfs-migrate(1)
    26  	// subcommand 'info' specifying a human-readable string threshold of
    27  	// filesize before entries are counted.
    28  	migrateInfoAboveFmt string
    29  	// migrateInfoAbove is the number of bytes parsed from the above
    30  	// migrateInfoAboveFmt flag.
    31  	migrateInfoAbove uint64
    32  
    33  	// migrateInfoUnitFmt is a flag given to the git-lfs-migrate(1)
    34  	// subcommand 'info' specifying a human-readable string of units with
    35  	// which to display the number of bytes.
    36  	migrateInfoUnitFmt string
    37  	// migrateInfoUnit is the number of bytes in the unit given as
    38  	// migrateInfoUnitFmt.
    39  	migrateInfoUnit uint64
    40  )
    41  
    42  func migrateInfoCommand(cmd *cobra.Command, args []string) {
    43  	l := tasklog.NewLogger(os.Stderr)
    44  
    45  	db, err := getObjectDatabase()
    46  	if err != nil {
    47  		ExitWithError(err)
    48  	}
    49  	defer db.Close()
    50  
    51  	rewriter := getHistoryRewriter(cmd, db, l)
    52  
    53  	exts := make(map[string]*MigrateInfoEntry)
    54  
    55  	above, err := humanize.ParseBytes(migrateInfoAboveFmt)
    56  	if err != nil {
    57  		ExitWithError(errors.Wrap(err, "cannot parse --above=<n>"))
    58  	}
    59  
    60  	if u := cmd.Flag("unit"); u.Changed {
    61  		unit, err := humanize.ParseByteUnit(u.Value.String())
    62  		if err != nil {
    63  			ExitWithError(errors.Wrap(err, "cannot parse --unit=<unit>"))
    64  		}
    65  
    66  		migrateInfoUnit = unit
    67  	}
    68  
    69  	migrateInfoAbove = above
    70  
    71  	migrate(args, rewriter, l, &githistory.RewriteOptions{
    72  		BlobFn: func(path string, b *gitobj.Blob) (*gitobj.Blob, error) {
    73  			ext := fmt.Sprintf("*%s", filepath.Ext(path))
    74  
    75  			if len(ext) > 1 {
    76  				entry := exts[ext]
    77  				if entry == nil {
    78  					entry = &MigrateInfoEntry{Qualifier: ext}
    79  				}
    80  
    81  				entry.Total++
    82  				entry.BytesTotal += b.Size
    83  
    84  				if b.Size > int64(migrateInfoAbove) {
    85  					entry.TotalAbove++
    86  					entry.BytesAbove += b.Size
    87  				}
    88  
    89  				exts[ext] = entry
    90  			}
    91  
    92  			return b, nil
    93  		},
    94  	})
    95  	l.Close()
    96  
    97  	entries := EntriesBySize(MapToEntries(exts))
    98  	entries = removeEmptyEntries(entries)
    99  	sort.Sort(sort.Reverse(entries))
   100  
   101  	migrateInfoTopN = tools.ClampInt(migrateInfoTopN, len(entries), 0)
   102  
   103  	entries = entries[:tools.MaxInt(0, migrateInfoTopN)]
   104  
   105  	entries.Print(os.Stdout)
   106  }
   107  
   108  // MigrateInfoEntry represents a tuple of filetype to bytes and entry count
   109  // above and below a threshold.
   110  type MigrateInfoEntry struct {
   111  	// Qualifier is the filepath's extension.
   112  	Qualifier string
   113  
   114  	// BytesAbove is total size of all files above a given threshold.
   115  	BytesAbove int64
   116  	// TotalAbove is the count of all files above a given size threshold.
   117  	TotalAbove int64
   118  	// BytesTotal is the number of bytes of all files
   119  	BytesTotal int64
   120  	// Total is the count of all files.
   121  	Total int64
   122  }
   123  
   124  // MapToEntries creates a set of `*MigrateInfoEntry`'s for a given map of
   125  // filepath extensions to file size in bytes.
   126  func MapToEntries(exts map[string]*MigrateInfoEntry) []*MigrateInfoEntry {
   127  	entries := make([]*MigrateInfoEntry, 0, len(exts))
   128  	for _, entry := range exts {
   129  		entries = append(entries, entry)
   130  	}
   131  
   132  	return entries
   133  }
   134  
   135  // removeEmptyEntries removes `*MigrateInfoEntry`'s for which no matching file
   136  // is above the given threshold "--above".
   137  func removeEmptyEntries(entries []*MigrateInfoEntry) []*MigrateInfoEntry {
   138  	nz := make([]*MigrateInfoEntry, 0, len(entries))
   139  	for _, e := range entries {
   140  		if e.TotalAbove > 0 {
   141  			nz = append(nz, e)
   142  		}
   143  	}
   144  
   145  	return nz
   146  }
   147  
   148  // EntriesBySize is an implementation of sort.Interface that sorts a set of
   149  // `*MigrateInfoEntry`'s
   150  type EntriesBySize []*MigrateInfoEntry
   151  
   152  // Len returns the total length of the set of `*MigrateInfoEntry`'s.
   153  func (e EntriesBySize) Len() int { return len(e) }
   154  
   155  // Less returns the whether or not the MigrateInfoEntry given at `i` takes up
   156  // less total size than the MigrateInfoEntry given at `j`.
   157  func (e EntriesBySize) Less(i, j int) bool { return e[i].BytesAbove < e[j].BytesAbove }
   158  
   159  // Swap swaps the entries given at i, j.
   160  func (e EntriesBySize) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
   161  
   162  // Print formats the `*MigrateInfoEntry`'s in the set and prints them to the
   163  // given io.Writer, "to", returning "n" the number of bytes written, and any
   164  // error, if one occurred.
   165  func (e EntriesBySize) Print(to io.Writer) (int, error) {
   166  	if len(e) == 0 {
   167  		return 0, nil
   168  	}
   169  
   170  	extensions := make([]string, 0, len(e))
   171  	sizes := make([]string, 0, len(e))
   172  	stats := make([]string, 0, len(e))
   173  	percentages := make([]string, 0, len(e))
   174  
   175  	for _, entry := range e {
   176  		bytesAbove := uint64(entry.BytesAbove)
   177  		above := entry.TotalAbove
   178  		total := entry.Total
   179  		percentAbove := 100 * (float64(above) / float64(total))
   180  
   181  		var size string
   182  		if migrateInfoUnit > 0 {
   183  			size = humanize.FormatBytesUnit(bytesAbove, migrateInfoUnit)
   184  		} else {
   185  			size = humanize.FormatBytes(bytesAbove)
   186  		}
   187  
   188  		stat := fmt.Sprintf("%d/%d files(s)",
   189  			above, total)
   190  
   191  		percentage := fmt.Sprintf("%.0f%%", percentAbove)
   192  
   193  		extensions = append(extensions, entry.Qualifier)
   194  		sizes = append(sizes, size)
   195  		stats = append(stats, stat)
   196  		percentages = append(percentages, percentage)
   197  	}
   198  
   199  	extensions = tools.Ljust(extensions)
   200  	sizes = tools.Ljust(sizes)
   201  	stats = tools.Rjust(stats)
   202  	percentages = tools.Rjust(percentages)
   203  
   204  	output := make([]string, 0, len(e))
   205  	for i := 0; i < len(e); i++ {
   206  		extension := extensions[i]
   207  		size := sizes[i]
   208  		stat := stats[i]
   209  		percentage := percentages[i]
   210  
   211  		line := strings.Join([]string{extension, size, stat, percentage}, "\t")
   212  
   213  		output = append(output, line)
   214  	}
   215  
   216  	return fmt.Fprintln(to, strings.Join(output, "\n"))
   217  }