Skip to content

Commit 76f94cb

Browse files
committed
Optimize who: parallel git calls, remove N per-author last-active lookups
1 parent 131fc55 commit 76f94cb

2 files changed

Lines changed: 100 additions & 45 deletions

File tree

cmd/who.go

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ func init() {
2525
}
2626

2727
type contributor struct {
28-
name string
29-
emails map[string]bool
30-
commits int
31-
added int
32-
deleted int
28+
name string
29+
emails map[string]bool
30+
commits int
31+
added int
32+
deleted int
33+
lastActive string // ISO date
3334
}
3435

3536
func runWho(cmd *cobra.Command, args []string) error {
@@ -51,19 +52,25 @@ func runWho(cmd *cobra.Command, args []string) error {
5152
func whoRepo(n int, since string) error {
5253
sp := ui.StartSpinner("Analyzing contributors (this may take a moment)...")
5354

54-
// Get commit counts + emails
55+
// Run both git commands in parallel
5556
shortlogArgs := []string{"shortlog", "-sne", "--all"}
5657
if since != "" {
5758
shortlogArgs = append(shortlogArgs, "--since="+since)
5859
}
59-
shortlogOut, err := git.Run(shortlogArgs...)
60-
61-
// Get line stats per author (added/deleted)
62-
numstatArgs := []string{"log", "--all", "--numstat", "--format=%aE"}
60+
shortstatArgs := []string{"log", "--all", "--shortstat", "--format=%aE|%aI"}
6361
if since != "" {
64-
numstatArgs = append(numstatArgs, "--since="+since)
62+
shortstatArgs = append(shortstatArgs, "--since="+since)
6563
}
66-
numstatOut := git.RunUnchecked(numstatArgs...)
64+
65+
var shortlogOut, numstatOut string
66+
var err error
67+
done := make(chan struct{})
68+
go func() {
69+
shortlogOut, err = git.Run(shortlogArgs...)
70+
close(done)
71+
}()
72+
numstatOut = git.RunUnchecked(shortstatArgs...)
73+
<-done
6774

6875
// Parse shortlog
6976
type rawEntry struct {
@@ -97,26 +104,44 @@ func whoRepo(n int, since string) error {
97104
}
98105
}
99106

100-
// Parse numstat to get lines per email
101-
linesByEmail := map[string][2]int{} // email -> [added, deleted]
107+
// Parse shortstat to get lines and last-active per email
108+
linesByEmail := map[string][2]int{} // email -> [added, deleted]
109+
lastActiveByEmail := map[string]string{} // email -> ISO date (first seen = most recent)
102110
if numstatOut != "" {
103-
var currentEmail string
111+
var currentEmail, currentDate string
104112
for _, line := range strings.Split(numstatOut, "\n") {
105113
line = strings.TrimSpace(line)
106114
if line == "" {
107115
continue
108116
}
109-
// Email lines from --format=%aE
110-
if !strings.Contains(line, "\t") && strings.Contains(line, "@") {
111-
currentEmail = strings.ToLower(line)
117+
// Format lines: "email|date" from --format=%aE|%aI
118+
if strings.Contains(line, "@") && strings.Contains(line, "|") {
119+
parts := strings.SplitN(line, "|", 2)
120+
currentEmail = strings.ToLower(parts[0])
121+
if len(parts) > 1 {
122+
currentDate = parts[1]
123+
}
124+
// Track first (most recent) date per email
125+
if _, exists := lastActiveByEmail[currentEmail]; !exists {
126+
lastActiveByEmail[currentEmail] = currentDate
127+
}
112128
continue
113129
}
114-
// Numstat lines: "added\tdeleted\tfile"
115-
parts := strings.SplitN(line, "\t", 3)
116-
if len(parts) >= 2 && currentEmail != "" {
130+
// Shortstat lines
131+
if strings.Contains(line, "file") && strings.Contains(line, "changed") && currentEmail != "" {
117132
var a, d int
118-
fmt.Sscanf(parts[0], "%d", &a)
119-
fmt.Sscanf(parts[1], "%d", &d)
133+
if idx := strings.Index(line, "insertion"); idx > 0 {
134+
sub := strings.TrimSpace(line[:idx])
135+
if ci := strings.LastIndex(sub, " "); ci >= 0 {
136+
fmt.Sscanf(strings.TrimSpace(sub[ci+1:]), "%d", &a)
137+
}
138+
}
139+
if idx := strings.Index(line, "deletion"); idx > 0 {
140+
sub := strings.TrimSpace(line[:idx])
141+
if ci := strings.LastIndex(sub, " "); ci >= 0 {
142+
fmt.Sscanf(strings.TrimSpace(sub[ci+1:]), "%d", &d)
143+
}
144+
}
120145
stats := linesByEmail[currentEmail]
121146
stats[0] += a
122147
stats[1] += d
@@ -192,12 +217,16 @@ func whoRepo(n int, since string) error {
192217
}
193218
for root, emails := range groupEmails {
194219
groups[root].emails = emails
195-
// Sum line stats across all emails in this group
196220
for email := range emails {
197221
if stats, ok := linesByEmail[email]; ok {
198222
groups[root].added += stats[0]
199223
groups[root].deleted += stats[1]
200224
}
225+
if date, ok := lastActiveByEmail[email]; ok {
226+
if groups[root].lastActive == "" || date > groups[root].lastActive {
227+
groups[root].lastActive = date
228+
}
229+
}
201230
}
202231
}
203232

@@ -255,7 +284,10 @@ func whoRepo(n int, since string) error {
255284
}
256285
sort.Strings(emailList)
257286

258-
lastActive := getAuthorLastEdit(c.name, ".")
287+
lastActive := "unknown"
288+
if c.lastActive != "" {
289+
lastActive = git.TimeAgo(c.lastActive)
290+
}
259291

260292
// Lines and percentage
261293
linesStr := ui.AddStyle.Render(fmt.Sprintf("+%d", c.added)) + " " + ui.DelStyle.Render(fmt.Sprintf("-%d", c.deleted))
@@ -290,10 +322,3 @@ func whoRepo(n int, since string) error {
290322
return nil
291323
}
292324

293-
func getAuthorLastEdit(author, path string) string {
294-
out := git.RunUnchecked("log", "-1", "--author="+author, "--format=%aI", "--", path)
295-
if out == "" {
296-
return "unknown"
297-
}
298-
return git.TimeAgo(out)
299-
}

internal/stack/tree.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ func BuildTree() *BranchStack {
5555
}
5656

5757
// Self-heal: detect parents for unknown branches
58+
// Only check against trunk and existing stack parents (not all branches - avoids O(N^2))
5859
changed := false
60+
candidates := map[string]bool{mainBranch: true}
61+
for _, meta := range cfg.Branches {
62+
candidates[meta.Parent] = true
63+
}
5964
for branch := range branchSHAs {
6065
if branch == mainBranch {
6166
continue
@@ -65,7 +70,9 @@ func BuildTree() *BranchStack {
6570
continue
6671
}
6772
}
68-
if detected := detectParent(branch, branchSHAs); detected != "" {
73+
// Only check against trunk and known parents, not all branches
74+
detected := detectParentFast(branch, mainBranch, candidates, branchSHAs)
75+
if detected != "" {
6976
parentHead, _ := git.MergeBase(branch, detected)
7077
cfg.Branches[branch] = &BranchMeta{Parent: detected, ParentHead: parentHead}
7178
changed = true
@@ -91,19 +98,29 @@ func BuildTree() *BranchStack {
9198
}
9299
}
93100

94-
// Calculate ahead/behind
101+
// Calculate ahead/behind and merged status
102+
// Batch merged check: one call per unique parent instead of per child
103+
mergedCache := map[string]map[string]bool{} // parent -> set of merged branches
95104
for childName, meta := range cfg.Branches {
96105
child, childOK := nodes[childName]
97106
_, parentOK := nodes[meta.Parent]
98107
if childOK && parentOK {
99108
child.Ahead, child.Behind = git.AheadBehind(childName, meta.Parent)
100109

101-
merged := git.RunUnchecked("branch", "--merged", meta.Parent)
102-
for _, line := range strings.Split(merged, "\n") {
103-
if strings.TrimSpace(strings.TrimLeft(line, "* ")) == childName {
104-
child.IsMerged = true
105-
break
110+
// Cache merged branches per parent
111+
if _, ok := mergedCache[meta.Parent]; !ok {
112+
mergedSet := map[string]bool{}
113+
out := git.RunUnchecked("branch", "--merged", meta.Parent)
114+
for _, line := range strings.Split(out, "\n") {
115+
name := strings.TrimSpace(strings.TrimLeft(line, "* "))
116+
if name != "" {
117+
mergedSet[name] = true
118+
}
106119
}
120+
mergedCache[meta.Parent] = mergedSet
121+
}
122+
if mergedCache[meta.Parent][childName] {
123+
child.IsMerged = true
107124
}
108125
}
109126
}
@@ -162,12 +179,22 @@ func BuildTree() *BranchStack {
162179
}
163180
}
164181

165-
func detectParent(branch string, branchSHAs map[string]string) string {
182+
// detectParentFast checks only trunk and known stack parents (not all branches).
183+
func detectParentFast(branch, mainBranch string, candidates map[string]bool, branchSHAs map[string]string) string {
184+
// Try trunk first (most common case)
185+
if _, exists := branchSHAs[mainBranch]; exists {
186+
if mb, err := git.MergeBase(branch, mainBranch); err == nil && mb != "" {
187+
return mainBranch
188+
}
189+
}
190+
// Try other known parents
166191
var bestParent string
167-
bestDistance := int(^uint(0) >> 1) // max int
168-
169-
for candidate := range branchSHAs {
170-
if candidate == branch {
192+
bestDistance := int(^uint(0) >> 1)
193+
for candidate := range candidates {
194+
if candidate == branch || candidate == mainBranch {
195+
continue
196+
}
197+
if _, exists := branchSHAs[candidate]; !exists {
171198
continue
172199
}
173200
mb, err := git.MergeBase(branch, candidate)
@@ -186,5 +213,8 @@ func detectParent(branch string, branchSHAs map[string]string) string {
186213
}
187214
}
188215
}
189-
return bestParent
216+
if bestParent != "" {
217+
return bestParent
218+
}
219+
return mainBranch
190220
}

0 commit comments

Comments
 (0)