Merge pull request #2324 from git-lfs/humanize-pkg
tools: implement and use 'humanize' package
This commit is contained in:
commit
73f1fc19c7
@ -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")
|
||||
|
97
tools/humanize/humanize.go
Normal file
97
tools/humanize/humanize.go
Normal file
@ -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)
|
||||
}
|
123
tools/humanize/humanize_test.go
Normal file
123
tools/humanize/humanize_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
5
tools/humanize/package.go
Normal file
5
tools/humanize/package.go
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user