Repository navigation
Expand file tree
/
Copy pathprogress.go
More file actions
114 lines (102 loc) · 3.48 KB
/
Copy pathprogress.go
File metadata and controls
114 lines (102 loc) · 3.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package main
import (
"fmt"
"io"
"sync"
"time"
)
const (
// progressInterval is the shortest time between rewrites of the progress line on a terminal, so that the
// counter never becomes the bottleneck of a collection.
progressInterval = 250 * time.Millisecond
// progressSteps is the number of lines a collection prints when stderr is not a terminal: one per tenth.
progressSteps = 10
percent = 100
)
// statusWriter is the writer for everything said about a collection's progress on stderr: the collector's
// messages and counters, and the transport's rate-limit notices, which can arrive from any goroutine. It keeps
// a counter being redrawn in place from being overwritten by a message, by finishing the line first, and it
// remembers whether anything was said, so that what follows can be set apart by a blank line only when there
// is something to set it apart from.
type statusWriter struct {
io.Writer
mu sync.Mutex
written bool // anything at all since the last separation
open bool // the last write left its line unfinished, as an in-place counter does
}
func (s *statusWriter) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.open && p[0] != '\r' {
_, _ = s.Writer.Write([]byte{'\n'})
}
s.written = true
s.open = p[len(p)-1] != '\n'
return s.Writer.Write(p) //nolint:wrapcheck // a plain pass-through
}
// separate finishes any unfinished line and ends the status output with a blank line if there was any, then
// forgets it all, so that a later collection starts afresh.
func (s *statusWriter) separate() {
s.mu.Lock()
defer s.mu.Unlock()
if s.open {
_, _ = s.Writer.Write([]byte{'\n'})
}
if s.written {
_, _ = s.Writer.Write([]byte{'\n'})
}
s.open, s.written = false, false
}
// progressReporter renders the collector's commit counter on stderr. On a terminal the line is rewritten in
// place a few times a second; anywhere else, such as a log or a pipe, a line is printed each time another tenth
// of the total completes, so the output stays readable without a carriage return.
type progressReporter struct {
w io.Writer
terminal bool
what string // what is counted, past tense, such as "commits fetched"
now func() time.Time
last time.Time // when the line was last rewritten on a terminal
shown uint // tenths of the total already printed off a terminal
}
func newProgressReporter(w io.Writer, terminal bool, what string) *progressReporter {
return &progressReporter{w: w, terminal: terminal, what: what, now: time.Now}
}
// update reports done of total; the collector calls it once per item.
func (p *progressReporter) update(done, total uint) {
if total == 0 {
return
}
if p.terminal {
p.rewrite(done, total)
} else {
p.step(done, total)
}
}
func (p *progressReporter) rewrite(done, total uint) {
now := p.now()
if done < total && now.Sub(p.last) < progressInterval {
return
}
p.last = now
end := ""
if done == total {
end = "\n"
}
_, _ = fmt.Fprintf(p.w, "\r%s%s", p.line(done, total), end)
}
func (p *progressReporter) step(done, total uint) {
tenths := done * progressSteps / total
if tenths <= p.shown {
return
}
p.shown = tenths
_, _ = fmt.Fprintln(p.w, p.line(done, total))
}
// line describes the progress so far; its length never shrinks as done grows, so rewriting it in place leaves
// nothing behind.
func (p *progressReporter) line(done, total uint) string {
return fmt.Sprintf("%d of %d %s (%d%%)...", done, total, p.what, done*percent/total)
}