Merge pull request #2324 from git-lfs/humanize-pkg

tools: implement and use 'humanize' package
This commit is contained in:
Taylor Blau 2017-06-13 14:39:50 -06:00 committed by GitHub
commit 73f1fc19c7
4 changed files with 229 additions and 23 deletions

@ -13,6 +13,7 @@ import (
"github.com/git-lfs/git-lfs/localstorage"
"github.com/git-lfs/git-lfs/progress"
"github.com/git-lfs/git-lfs/tools"
"github.com/git-lfs/git-lfs/tools/humanize"
"github.com/git-lfs/git-lfs/tq"
"github.com/rubyist/tracerx"
"github.com/spf13/cobra"
@ -142,7 +143,7 @@ func prune(fetchPruneConfig config.FetchPruneConfig, verifyRemote, dryRun, verbo
totalSize += file.Size
if verbose {
// Save up verbose output for the end, spinner still going
verboseOutput.WriteString(fmt.Sprintf(" * %v (%v)\n", file.Oid, humanizeBytes(file.Size)))
verboseOutput.WriteString(fmt.Sprintf(" * %v (%v)\n", file.Oid, humanize.FormatBytes(uint64(file.Size))))
}
if verifyRemote {
@ -171,12 +172,12 @@ func prune(fetchPruneConfig config.FetchPruneConfig, verifyRemote, dryRun, verbo
return
}
if dryRun {
Print("%d files would be pruned (%v)", len(prunableObjects), humanizeBytes(totalSize))
Print("%d files would be pruned (%v)", len(prunableObjects), humanize.FormatBytes(uint64(totalSize)))
if verbose {
Print(verboseOutput.String())
}
} else {
Print("Pruning %d files, (%v)", len(prunableObjects), humanizeBytes(totalSize))
Print("Pruning %d files, (%v)", len(prunableObjects), humanize.FormatBytes(uint64(totalSize)))
if verbose {
Print(verboseOutput.String())
}
@ -461,26 +462,6 @@ func pruneTaskGetReachableObjects(gitscanner *lfs.GitScanner, outObjectSet *tool
}
}
var byteUnits = []string{"B", "KB", "MB", "GB", "TB"}
func humanizeBytes(bytes int64) string {
var output string
size := float64(bytes)
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
for _, unit := range byteUnits {
if size < 1024.0 {
output = fmt.Sprintf("%3.1f %s", size, unit)
break
}
size /= 1024.0
}
return output
}
func init() {
RegisterCommand("prune", pruneCommand, func(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&pruneDryRunArg, "dry-run", "d", false, "Don't delete anything, just report")

@ -0,0 +1,97 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
"github.com/git-lfs/git-lfs/errors"
)
const (
Byte = 1 << (iota * 10)
Kibibyte
Mebibyte
Gibibyte
Tebibyte
Pebibyte
Kilobyte = 1000 * Byte
Megabyte = 1000 * Kilobyte
Gigabyte = 1000 * Megabyte
Terabyte = 1000 * Gigabyte
Petabyte = 1000 * Terabyte
)
var bytesTable = map[string]uint64{
"b": Byte,
"kib": Kibibyte,
"mib": Mebibyte,
"gib": Gibibyte,
"tib": Tebibyte,
"pib": Pebibyte,
"kb": Kilobyte,
"mb": Megabyte,
"gb": Gigabyte,
"tb": Terabyte,
"pb": Petabyte,
}
// ParseBytes parses a given human-readable bytes or ibytes string into a number
// of bytes, or an error if the string was unable to be parsed.
func ParseBytes(str string) (uint64, error) {
var sep int
for _, r := range str {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
sep = sep + 1
}
f, err := strconv.ParseFloat(strings.Replace(str[:sep], ",", "", -1), 64)
if err != nil {
return 0, err
}
unit := strings.ToLower(strings.TrimSpace(str[sep:]))
if m, ok := bytesTable[unit]; ok {
f = f * float64(m)
if f >= math.MaxUint64 {
return 0, errors.New("number of bytes too large")
}
return uint64(f), nil
}
return 0, errors.Errorf("unknown unit: %q", unit)
}
var sizes = []string{"B", "KB", "MB", "GB", "TB", "PB"}
// FormatBytes outputs the given number of bytes "s" as a human-readable string,
// rounding to the nearest half within .01.
func FormatBytes(s uint64) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(log(float64(s), 1000))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(1000, e)*10+.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// log takes the log base "b" of "n" (\log_b{n})
func log(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}

@ -0,0 +1,123 @@
package humanize_test
import (
"math"
"testing"
"github.com/git-lfs/git-lfs/tools/humanize"
"github.com/stretchr/testify/assert"
)
type ParseBytesTestCase struct {
Given string
Expected uint64
Err error
}
func (c *ParseBytesTestCase) Assert(t *testing.T) {
got, err := humanize.ParseBytes(c.Given)
if c.Err == nil {
assert.NoError(t, err, "unexpected error: %s", err)
assert.EqualValues(t, c.Expected, got)
} else {
assert.Equal(t, c.Err, err)
}
}
type FormatBytesTestCase struct {
Given uint64
Expected string
}
func (c *FormatBytesTestCase) Assert(t *testing.T) {
assert.Equal(t, c.Expected, humanize.FormatBytes(c.Given))
}
func TestParseBytes(t *testing.T) {
for desc, c := range map[string]*ParseBytesTestCase{
"parse byte": {"10B", uint64(10 * math.Pow(2, 0)), nil},
"parse kibibyte": {"20KIB", uint64(20 * math.Pow(2, 10)), nil},
"parse mebibyte": {"30MIB", uint64(30 * math.Pow(2, 20)), nil},
"parse gibibyte": {"40GIB", uint64(40 * math.Pow(2, 30)), nil},
"parse tebibyte": {"50TIB", uint64(50 * math.Pow(2, 40)), nil},
"parse pebibyte": {"60PIB", uint64(60 * math.Pow(2, 50)), nil},
"parse byte (lowercase)": {"10b", uint64(10 * math.Pow(2, 0)), nil},
"parse kibibyte (lowercase)": {"20kib", uint64(20 * math.Pow(2, 10)), nil},
"parse mebibyte (lowercase)": {"30mib", uint64(30 * math.Pow(2, 20)), nil},
"parse gibibyte (lowercase)": {"40gib", uint64(40 * math.Pow(2, 30)), nil},
"parse tebibyte (lowercase)": {"50tib", uint64(50 * math.Pow(2, 40)), nil},
"parse pebibyte (lowercase)": {"60pib", uint64(60 * math.Pow(2, 50)), nil},
"parse byte (with space)": {"10 B", uint64(10 * math.Pow(2, 0)), nil},
"parse kibibyte (with space)": {"20 KIB", uint64(20 * math.Pow(2, 10)), nil},
"parse mebibyte (with space)": {"30 MIB", uint64(30 * math.Pow(2, 20)), nil},
"parse gibibyte (with space)": {"40 GIB", uint64(40 * math.Pow(2, 30)), nil},
"parse tebibyte (with space)": {"50 TIB", uint64(50 * math.Pow(2, 40)), nil},
"parse pebibyte (with space)": {"60 PIB", uint64(60 * math.Pow(2, 50)), nil},
"parse byte (with space, lowercase)": {"10 b", uint64(10 * math.Pow(2, 0)), nil},
"parse kibibyte (with space, lowercase)": {"20 kib", uint64(20 * math.Pow(2, 10)), nil},
"parse mebibyte (with space, lowercase)": {"30 mib", uint64(30 * math.Pow(2, 20)), nil},
"parse gibibyte (with space, lowercase)": {"40 gib", uint64(40 * math.Pow(2, 30)), nil},
"parse tebibyte (with space, lowercase)": {"50 tib", uint64(50 * math.Pow(2, 40)), nil},
"parse pebibyte (with space, lowercase)": {"60 pib", uint64(60 * math.Pow(2, 50)), nil},
"parse kilobyte": {"20KB", uint64(20 * math.Pow(10, 3)), nil},
"parse megabyte": {"30MB", uint64(30 * math.Pow(10, 6)), nil},
"parse gigabyte": {"40GB", uint64(40 * math.Pow(10, 9)), nil},
"parse terabyte": {"50TB", uint64(50 * math.Pow(10, 12)), nil},
"parse petabyte": {"60PB", uint64(60 * math.Pow(10, 15)), nil},
"parse kilobyte (lowercase)": {"20kb", uint64(20 * math.Pow(10, 3)), nil},
"parse megabyte (lowercase)": {"30mb", uint64(30 * math.Pow(10, 6)), nil},
"parse gigabyte (lowercase)": {"40gb", uint64(40 * math.Pow(10, 9)), nil},
"parse terabyte (lowercase)": {"50tb", uint64(50 * math.Pow(10, 12)), nil},
"parse petabyte (lowercase)": {"60pb", uint64(60 * math.Pow(10, 15)), nil},
"parse kilobyte (with space)": {"20 KB", uint64(20 * math.Pow(10, 3)), nil},
"parse megabyte (with space)": {"30 MB", uint64(30 * math.Pow(10, 6)), nil},
"parse gigabyte (with space)": {"40 GB", uint64(40 * math.Pow(10, 9)), nil},
"parse terabyte (with space)": {"50 TB", uint64(50 * math.Pow(10, 12)), nil},
"parse petabyte (with space)": {"60 PB", uint64(60 * math.Pow(10, 15)), nil},
"parse kilobyte (with space, lowercase)": {"20 kb", uint64(20 * math.Pow(10, 3)), nil},
"parse megabyte (with space, lowercase)": {"30 mb", uint64(30 * math.Pow(10, 6)), nil},
"parse gigabyte (with space, lowercase)": {"40 gb", uint64(40 * math.Pow(10, 9)), nil},
"parse terabyte (with space, lowercase)": {"50 tb", uint64(50 * math.Pow(10, 12)), nil},
"parse petabyte (with space, lowercase)": {"60 pb", uint64(60 * math.Pow(10, 15)), nil},
} {
t.Run(desc, c.Assert)
}
}
func TestFormatBytes(t *testing.T) {
for desc, c := range map[string]*FormatBytesTestCase{
"format bytes": {uint64(1 * math.Pow(10, 0)), "1 B"},
"format kilobytes": {uint64(1 * math.Pow(10, 3)), "1.0 KB"},
"format megabytes": {uint64(1 * math.Pow(10, 6)), "1.0 MB"},
"format gigabytes": {uint64(1 * math.Pow(10, 9)), "1.0 GB"},
"format petabytes": {uint64(1 * math.Pow(10, 12)), "1.0 TB"},
"format terabytes": {uint64(1 * math.Pow(10, 15)), "1.0 PB"},
"format kilobytes under": {uint64(1.49 * math.Pow(10, 3)), "1.5 KB"},
"format megabytes under": {uint64(1.49 * math.Pow(10, 6)), "1.5 MB"},
"format gigabytes under": {uint64(1.49 * math.Pow(10, 9)), "1.5 GB"},
"format petabytes under": {uint64(1.49 * math.Pow(10, 12)), "1.5 TB"},
"format terabytes under": {uint64(1.49 * math.Pow(10, 15)), "1.5 PB"},
"format kilobytes over": {uint64(1.51 * math.Pow(10, 3)), "1.5 KB"},
"format megabytes over": {uint64(1.51 * math.Pow(10, 6)), "1.5 MB"},
"format gigabytes over": {uint64(1.51 * math.Pow(10, 9)), "1.5 GB"},
"format petabytes over": {uint64(1.51 * math.Pow(10, 12)), "1.5 TB"},
"format terabytes over": {uint64(1.51 * math.Pow(10, 15)), "1.5 PB"},
"format kilobytes exact": {uint64(1.3 * math.Pow(10, 3)), "1.3 KB"},
"format megabytes exact": {uint64(1.3 * math.Pow(10, 6)), "1.3 MB"},
"format gigabytes exact": {uint64(1.3 * math.Pow(10, 9)), "1.3 GB"},
"format petabytes exact": {uint64(1.3 * math.Pow(10, 12)), "1.3 TB"},
"format terabytes exact": {uint64(1.3 * math.Pow(10, 15)), "1.3 PB"},
} {
t.Run(desc, c.Assert)
}
}

@ -0,0 +1,5 @@
// package humanize is designed to parse and format "humanized" versions of
// numbers with units.
//
// Based on: github.com/dustin/go-humanize.
package humanize