diff --git a/commands/command_migrate.go b/commands/command_migrate.go index de01f760..0b2fec63 100644 --- a/commands/command_migrate.go +++ b/commands/command_migrate.go @@ -208,6 +208,7 @@ func getHistoryRewriter(cmd *cobra.Command, db *odb.ObjectDatabase) *githistory. func init() { info := NewCommand("info", migrateInfoCommand) info.Flags().IntVar(&migrateInfoTopN, "top", 5, "--top=") + info.Flags().StringVar(&migrateInfoAboveFmt, "above", "0 B", "--above=") RegisterCommand("migrate", nil, func(cmd *cobra.Command) { // Adding flags directly to cmd.Flags() doesn't apply those diff --git a/commands/command_migrate_info.go b/commands/command_migrate_info.go index 0a39afa5..c1ca9429 100644 --- a/commands/command_migrate_info.go +++ b/commands/command_migrate_info.go @@ -8,8 +8,10 @@ import ( "sort" "strings" + "github.com/git-lfs/git-lfs/errors" "github.com/git-lfs/git-lfs/git/odb" "github.com/git-lfs/git-lfs/tools" + "github.com/git-lfs/git-lfs/tools/humanize" "github.com/spf13/cobra" ) @@ -17,15 +19,45 @@ var ( // migrateInfoTopN is a flag given to the git-lfs-migrate(1) subcommand // 'info' which specifies how many info entries to show by default. migrateInfoTopN int + + // migrateInfoAboveFmt is a flag given to the git-lfs-migrate(1) + // subcommand 'info' specifying a human-readable string threshold of + // filesize before entries are counted. + migrateInfoAboveFmt string + // migrateInfoAbove is the number of bytes parsed from the above + // migrateInfoAboveFmt flag. + migrateInfoAbove uint64 ) func migrateInfoCommand(cmd *cobra.Command, args []string) { - exts := make(map[string]int64) + exts := make(map[string]*MigrateInfoEntry) + + above, err := humanize.ParseBytes(migrateInfoAboveFmt) + if err != nil { + ExitWithError(errors.Wrap(err, "cannot parse --above=")) + } + + migrateInfoAbove = above migrate(cmd, args, func(path string, b *odb.Blob) (*odb.Blob, error) { ext := fmt.Sprintf("*%s", filepath.Ext(path)) + if len(ext) > 1 { - exts[ext] = exts[ext] + b.Size + entry := exts[ext] + if entry == nil { + entry = &MigrateInfoEntry{Qualifier: ext} + } + + entry.Total++ + entry.BytesTotal += b.Size + + if b.Size > int64(migrateInfoAbove) { + entry.TotalAbove++ + entry.BytesAbove += b.Size + } + + // TODO(@ttaylorr): needed? + exts[ext] = entry } return b, nil @@ -46,19 +78,19 @@ func migrateInfoCommand(cmd *cobra.Command, args []string) { type MigrateInfoEntry struct { // Qualifier is the filepath's extension. Qualifier string - // Size is the total size in bytes taken by that filepath's extension. - Size int64 + + BytesAbove int64 + TotalAbove int64 + BytesTotal int64 + Total int64 } // MapToEntries creates a set of `*MigrateInfoEntry`'s for a given map of // filepath extensions to file size in bytes. -func MapToEntries(exts map[string]int64) []*MigrateInfoEntry { +func MapToEntries(exts map[string]*MigrateInfoEntry) []*MigrateInfoEntry { entries := make([]*MigrateInfoEntry, 0, len(exts)) - for qualifier, size := range exts { - entries = append(entries, &MigrateInfoEntry{ - Qualifier: qualifier, - Size: size, - }) + for _, entry := range exts { + entries = append(entries, entry) } return entries @@ -73,39 +105,57 @@ func (e EntriesBySize) Len() int { return len(e) } // Less returns the whether or not the MigrateInfoEntry given at `i` takes up // less total size than the MigrateInfoEntry given at `j`. -func (e EntriesBySize) Less(i, j int) bool { return e[i].Size < e[j].Size } +func (e EntriesBySize) Less(i, j int) bool { return e[i].BytesAbove < e[j].BytesAbove } // Swap swaps the entries given at i, j. func (e EntriesBySize) Swap(i, j int) { e[i], e[j] = e[j], e[i] } -// LongestQualifier returns the string length of the longest qualifier in the -// set. -func (e EntriesBySize) LongestQualifier() int { - var longest int - for _, entry := range e { - longest = tools.MaxInt(longest, len(entry.Qualifier)) - } - - return longest -} - // Print formats the `*MigrateInfoEntry`'s in the set and prints them to the // given io.Writer, "to", returning "n" the number of bytes written, and any // error, if one occurred. func (e EntriesBySize) Print(to io.Writer) (int, error) { - output := make([]string, 0, len(e)) - - longest := e.LongestQualifier() - + extensions := make([]string, 0, len(e)) for _, entry := range e { - offset := longest - len(entry.Qualifier) - padding := strings.Repeat(" ", tools.MaxInt(0, offset)) - bytes := humanizeBytes(entry.Size) + extensions = append(extensions, entry.Qualifier) + } + extensions = tools.Ljust(extensions) - line := fmt.Sprintf("%s%s %s", entry.Qualifier, padding, bytes) + files := make([]string, 0, len(e)) + for _, entry := range e { + bytes := humanize.FormatBytes(uint64(entry.BytesAbove)) + above := entry.TotalAbove + total := entry.Total + + file := fmt.Sprintf("%s, %d/%d files(s)", + bytes, above, total) + + files = append(files, file) + } + files = tools.Rjust(files) + + percentages := make([]string, 0, len(e)) + for _, entry := range e { + percentAbove := 100 * (float64(entry.TotalAbove) / float64(entry.Total)) + + percentage := fmt.Sprintf("%.0f%%", percentAbove) + + percentages = append(percentages, percentage) + } + percentages = tools.Rjust(percentages) + + output := make([]string, 0, len(e)) + for i := 0; i < len(e); i++ { + extension := extensions[i] + fileCount := files[i] + percentage := percentages[i] + + line := strings.Join([]string{extension, fileCount, percentage}, "\t") output = append(output, line) } + header := fmt.Sprintf("Files above %s:", humanize.FormatBytes(migrateInfoAbove)) + output = append([]string{header}, output...) + return fmt.Fprintln(to, strings.Join(output, "\n")) } diff --git a/test/test-migrate-info.sh b/test/test-migrate-info.sh index 46718f1c..fb196d5a 100755 --- a/test/test-migrate-info.sh +++ b/test/test-migrate-info.sh @@ -12,8 +12,9 @@ begin_test "migrate info (default branch)" original_head="$(git rev-parse HEAD)" diff -u <(git lfs migrate info 2>&1) <(cat <<-EOF - *.md 140 B - *.txt 120 B + Files above 0 B: + *.md 140 B, 1/1 files(s) 100% + *.txt 120 B, 1/1 files(s) 100% EOF) migrated_head="$(git rev-parse HEAD)" @@ -32,8 +33,9 @@ begin_test "migrate info (given branch)" original_feature="$(git rev-parse refs/heads/my-feature)" diff -u <(git lfs migrate info my-feature 2>&1) <(cat <<-EOF - *.md 170 B - *.txt 120 B + Files above 0 B: + *.md 170 B, 2/2 files(s) 100% + *.txt 120 B, 1/1 files(s) 100% EOF) migrated_master="$(git rev-parse refs/heads/master)" @@ -53,7 +55,8 @@ begin_test "migrate info (default branch with filter)" original_head="$(git rev-parse HEAD)" diff -u <(git lfs migrate info --include "*.md" 2>&1) <(cat <<-EOF - *.md 140 B + Files above 0 B: + *.md 140 B, 1/1 files(s) 100% EOF) migrated_head="$(git rev-parse HEAD)" @@ -72,7 +75,8 @@ begin_test "migrate info (given branch with filter)" original_feature="$(git rev-parse refs/heads/my-feature)" diff -u <(git lfs migrate info --include "*.md" my-feature 2>&1) <(cat <<-EOF - *.md 170 B + Files above 0 B: + *.md 170 B, 2/2 files(s) 100% EOF) migrated_master="$(git rev-parse refs/heads/master)" @@ -95,8 +99,9 @@ begin_test "migrate info (default branch, exclude remote refs)" original_master="$(git rev-parse refs/heads/master)" diff -u <(git lfs migrate info 2>&1) <(cat <<-EOF - *.md 50 B - *.txt 30 B + Files above 0 B: + *.md 50 B, 1/1 files(s) 100% + *.txt 30 B, 1/1 files(s) 100% EOF) migrated_remote="$(git rev-parse refs/remotes/origin/master)" @@ -118,8 +123,9 @@ begin_test "migrate info (given branch, exclude remote refs)" original_feature="$(git rev-parse refs/heads/my-feature)" diff -u <(git lfs migrate info my-feature 2>&1) <(cat <<-EOF - *.md 52 B - *.txt 50 B + Files above 0 B: + *.md 52 B, 2/2 files(s) 100% + *.txt 50 B, 2/2 files(s) 100% EOF) migrated_remote="$(git rev-parse refs/remotes/origin/master)" @@ -144,8 +150,9 @@ begin_test "migrate info (include/exclude ref)" diff -u <(git lfs migrate info \ --include-ref=refs/heads/my-feature \ --exclude-ref=refs/heads/master 2>&1) <(cat <<-EOF - *.md 31 B - *.txt 30 B + Files above 0 B: + *.md 31 B, 1/1 files(s) 100% + *.txt 30 B, 1/1 files(s) 100% EOF) migrated_master="$(git rev-parse refs/heads/master)" @@ -169,7 +176,8 @@ begin_test "migrate info (include/exclude ref with filter)" --include="*.txt" \ --include-ref=refs/heads/my-feature \ --exclude-ref=refs/heads/master 2>&1) <(cat <<-EOF - *.txt 30 B + Files above 0 B: + *.txt 30 B, 1/1 files(s) 100% EOF) migrated_master="$(git rev-parse refs/heads/master)" @@ -179,3 +187,43 @@ begin_test "migrate info (include/exclude ref with filter)" assert_ref_unmoved "refs/heads/my-feature" "$original_feature" "$migrated_feature" ) end_test + +begin_test "migrate info (above threshold)" +( + set -e + + setup_multiple_local_branches + + original_head="$(git rev-parse HEAD)" + + diff -u <(git lfs migrate info --above=130B 2>&1) <(cat <<-EOF + Files above 130 B: + *.md 140 B, 1/1 files(s) 100% + *.txt 0 B, 0/1 files(s) 0% + EOF) + + migrated_head="$(git rev-parse HEAD)" + + assert_ref_unmoved "HEAD" "$original_head" "$migrated_head" +) +end_test + +begin_test "migrate info (above threshold, top)" +( + set -e + + setup_multiple_local_branches + + original_head="$(git rev-parse HEAD)" + + diff -u <(git lfs migrate info --above=130B --top=1 2>&1) <(cat <<-EOF + Files above 130 B: + *.md 140 B, 1/1 files(s) 100% + EOF) + + migrated_head="$(git rev-parse HEAD)" + + assert_ref_unmoved "HEAD" "$original_head" "$migrated_head" +) +end_test +