Skip to content

Commit de2173c

Browse files
committed
fix(tui): key mismatch in gitChangesMap/updatesMap, source from .git, split fast/slow checks
- gitChangesMap and updatesMap were stored with shortName key but looked up with item.Name (full @owdproject/... scope). Result: * changes and update badges NEVER appeared on any row. Fixed by using item.ShortName in all lookups. - source column: replaced unreliable LastInstallChoices.gitUrl heuristic with localGitDirs[shortName] populated by checkLocalChangesCmd (os.Stat .git check). Now: npm = from registry, git = local folder with .git, dev = local folder no .git. - Split update check into two separate commands: * checkLocalChangesCmd: fast, local-only git status, no network, every ~15s. Also populates localGitDirs for the source column. * checkRemoteUpdatesCmd: slow, uses cached remote refs (no git fetch) + npm HTTP, every ~5min. Concurrency cap reduced to 5 since these are network ops. - Added runGitBehindCheckNoFetch: behind check without git fetch (uses cached refs).
1 parent 27d235a commit de2173c

4 files changed

Lines changed: 136 additions & 69 deletions

File tree

go/tui/helpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,37 @@ func runGitBehindCheck(dir string) (int, error) {
235235
return behind, err
236236
}
237237

238+
// runGitBehindCheckNoFetch checks how many commits behind the remote the local repo is,
239+
// WITHOUT running git fetch. Uses already-cached remote refs only — no network activity.
240+
func runGitBehindCheckNoFetch(dir string) (int, error) {
241+
cmdRev := exec.Command("git", "rev-list", "--count", "HEAD..@{u}")
242+
cmdRev.Dir = dir
243+
out, err := cmdRev.Output()
244+
if err != nil {
245+
// Fallback: try origin/<branch>
246+
cmdBranch := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
247+
cmdBranch.Dir = dir
248+
branchOut, errBranch := cmdBranch.Output()
249+
if errBranch != nil {
250+
return 0, errBranch
251+
}
252+
branch := strings.TrimSpace(string(branchOut))
253+
if branch == "" || branch == "HEAD" {
254+
return 0, err
255+
}
256+
cmdRev = exec.Command("git", "rev-list", "--count", fmt.Sprintf("HEAD..origin/%s", branch))
257+
cmdRev.Dir = dir
258+
out, err = cmdRev.Output()
259+
if err != nil {
260+
return 0, err
261+
}
262+
}
263+
var behind int
264+
_, err = fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &behind)
265+
return behind, err
266+
}
267+
268+
238269
func fetchLatestNpmVersion(pkgName string) (string, error) {
239270
client := &http.Client{Timeout: 3 * time.Second}
240271
resp, err := client.Get(fmt.Sprintf("https://registry.npmjs.org/%s/latest", url.PathEscape(pkgName)))

go/tui/tui.go

Lines changed: 90 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func NewModel(root string) TuiModel {
2424
memHistory: []int{},
2525
gitChangesMap: make(map[string]GitChanges),
2626
updatesMap: make(map[string]UpdateInfo),
27+
localGitDirs: make(map[string]bool),
2728
settingsSel: 0,
2829
termWidth: 160,
2930
termHeight: 40,
@@ -156,10 +157,12 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
156157
if m.tickCount%4 == 0 {
157158
m.blink = !m.blink
158159
}
159-
160-
var statusCmd tea.Cmd
160+
161+
var cmds []tea.Cmd
162+
cmds = append(cmds, tickCmd())
163+
164+
// Every ~1.5s: sample memory + check server status
161165
if m.tickCount%10 == 0 {
162-
// Sample Nuxt process memory stats
163166
var mb int
164167
if m.ctx != nil {
165168
pidPath := filepath.Join(m.ctx.Paths.MetaDir, "dev.pid")
@@ -175,22 +178,22 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175178
m.memHistory = m.memHistory[1:]
176179
}
177180
if m.ctx != nil {
178-
statusCmd = m.checkServerStatusCmd()
181+
cmds = append(cmds, m.checkServerStatusCmd())
179182
}
180183
}
181-
184+
185+
// Every ~15s: fast local git status check (no network)
182186
if m.tickCount%100 == 0 && !m.loading && m.activeTask == TaskNone && !m.checkingUpdates {
183187
m.checkingUpdates = true
184-
if statusCmd != nil {
185-
return m, tea.Batch(tickCmd(), statusCmd, m.checkForUpdatesCmd())
186-
}
187-
return m, tea.Batch(tickCmd(), m.checkForUpdatesCmd())
188+
cmds = append(cmds, m.checkLocalChangesCmd())
188189
}
189-
190-
if statusCmd != nil {
191-
return m, tea.Batch(tickCmd(), statusCmd)
190+
191+
// Every ~5min: slow remote check (git behind without fetch + npm versions)
192+
if m.tickCount%2000 == 0 && !m.loading && m.activeTask == TaskNone {
193+
cmds = append(cmds, m.checkRemoteUpdatesCmd())
192194
}
193-
return m, tickCmd()
195+
196+
return m, tea.Batch(cmds...)
194197

195198
case tea.KeyMsg:
196199
promptToShow := m.activePrompt
@@ -373,28 +376,28 @@ func (m *TuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
373376
m.catalog = msg.Cat
374377
m.statusMsg = fmt.Sprintf("Catalog loaded (%s).", msg.Cat.CacheAge)
375378

379+
// Run fast local check immediately; schedule remote check too
376380
m.checkingUpdates = true
377-
return m, m.checkForUpdatesCmd()
381+
return m, tea.Batch(m.checkLocalChangesCmd(), m.checkRemoteUpdatesCmd())
378382
}
379383

380-
case updatesLoadedMsg:
384+
case localChangesMsg:
381385
m.gitChangesMap = msg.GitChanges
382-
m.updatesMap = msg.Updates
386+
m.localGitDirs = msg.LocalGitDirs
383387
m.checkingUpdates = false
384388
m.loading = false
385-
if msg.Err != nil {
386-
m.statusMsg = fmt.Sprintf("⚠️ Updates check failed: %v", msg.Err)
387-
} else {
389+
390+
case remoteUpdatesMsg:
391+
m.updatesMap = msg.Updates
392+
if msg.Err == nil {
388393
count := 0
389394
for _, up := range msg.Updates {
390395
if up.LocalGit || up.Npm {
391396
count++
392397
}
393398
}
394399
if count > 0 {
395-
m.statusMsg = fmt.Sprintf("Updates check completed: %d update(s) available.", count)
396-
} else {
397-
m.statusMsg = "All packages are up to date."
400+
m.statusMsg = fmt.Sprintf("%d update(s) available.", count)
398401
}
399402
}
400403

@@ -488,24 +491,80 @@ func (m *TuiModel) addLog(line string) {
488491
}
489492

490493

491-
func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
494+
// checkLocalChangesCmd runs a fast local-only git status check for every catalog entry.
495+
// No network activity — only reads the local working tree.
496+
// Runs every ~15s. Also detects which packages have their own .git folder.
497+
func (m *TuiModel) checkLocalChangesCmd() tea.Cmd {
492498
return func() tea.Msg {
493499
gitChangesMap := make(map[string]GitChanges)
500+
localGitDirs := make(map[string]bool)
501+
502+
if m.catalog == nil {
503+
return localChangesMsg{GitChanges: gitChangesMap, LocalGitDirs: localGitDirs}
504+
}
505+
506+
for _, entry := range m.catalog.Entries {
507+
short := entry.ShortName
508+
var kindDir string
509+
switch entry.Kind {
510+
case "app":
511+
kindDir = "apps"
512+
case "module":
513+
kindDir = "packages"
514+
case "theme":
515+
kindDir = "themes"
516+
}
517+
if kindDir == "" {
518+
continue
519+
}
520+
521+
pkgPath := filepath.Join(m.workspaceRoot, kindDir, short)
522+
gitDir := filepath.Join(pkgPath, ".git")
523+
524+
if _, err := os.Stat(gitDir); err == nil {
525+
// Package has its own .git repo
526+
localGitDirs[short] = true
527+
added, modified, deleted, err := runGitStatus(pkgPath)
528+
if err == nil && (added > 0 || modified > 0 || deleted > 0) {
529+
gitChangesMap[short] = GitChanges{Added: added, Modified: modified, Deleted: deleted}
530+
}
531+
} else {
532+
// Fallback: check monorepo git for this subdirectory
533+
parentGitDir := filepath.Join(m.workspaceRoot, ".git")
534+
if _, err := os.Stat(parentGitDir); err == nil {
535+
relPath := filepath.Join(kindDir, short)
536+
added, modified, deleted, err := runGitStatusForSubdir(m.workspaceRoot, relPath)
537+
if err == nil && (added > 0 || modified > 0 || deleted > 0) {
538+
gitChangesMap[short] = GitChanges{Added: added, Modified: modified, Deleted: deleted}
539+
}
540+
}
541+
}
542+
}
543+
544+
return localChangesMsg{GitChanges: gitChangesMap, LocalGitDirs: localGitDirs}
545+
}
546+
}
547+
548+
// checkRemoteUpdatesCmd runs the slow network-dependent checks:
549+
// - git behind count (uses cached remote refs, NO git fetch)
550+
// - npm latest version check
551+
// Runs every ~5 minutes.
552+
func (m *TuiModel) checkRemoteUpdatesCmd() tea.Cmd {
553+
return func() tea.Msg {
494554
updatesMap := make(map[string]UpdateInfo)
495555

496556
if m.catalog == nil {
497-
return updatesLoadedMsg{GitChanges: gitChangesMap, Updates: updatesMap}
557+
return remoteUpdatesMsg{Updates: updatesMap}
498558
}
499559

500560
type result struct {
501561
shortName string
502-
changes *GitChanges
503562
update *UpdateInfo
504563
err error
505564
}
506565

507566
resultsChan := make(chan result, len(m.catalog.Entries))
508-
sem := make(chan struct{}, 10) // Limit concurrency
567+
sem := make(chan struct{}, 5) // modest concurrency — these are network ops
509568

510569
for _, e := range m.catalog.Entries {
511570
sem <- struct{}{}
@@ -514,9 +573,8 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
514573

515574
res := result{shortName: entry.ShortName}
516575

517-
// 1. Check local changes & upstream behind counts
518576
short := entry.ShortName
519-
kindDir := ""
577+
var kindDir string
520578
switch entry.Kind {
521579
case "app":
522580
kindDir = "apps"
@@ -526,33 +584,19 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
526584
kindDir = "themes"
527585
}
528586

587+
// 1. Git behind check (NO fetch — uses cached remote refs)
529588
if kindDir != "" {
530589
pkgPath := filepath.Join(m.workspaceRoot, kindDir, short)
531590
gitDir := filepath.Join(pkgPath, ".git")
532591
if _, err := os.Stat(gitDir); err == nil {
533-
added, modified, deleted, err := runGitStatus(pkgPath)
534-
if err == nil && (added > 0 || modified > 0 || deleted > 0) {
535-
res.changes = &GitChanges{Added: added, Modified: modified, Deleted: deleted}
536-
}
537-
538-
behind, err := runGitBehindCheck(pkgPath)
592+
behind, err := runGitBehindCheckNoFetch(pkgPath)
539593
if err == nil && behind > 0 {
540594
res.update = &UpdateInfo{LocalGit: true, BehindCount: behind}
541595
}
542-
} else {
543-
// Fallback: Check if the monorepo itself tracks changes for this directory
544-
parentGitDir := filepath.Join(m.workspaceRoot, ".git")
545-
if _, err := os.Stat(parentGitDir); err == nil {
546-
relPath := filepath.Join(kindDir, short)
547-
added, modified, deleted, err := runGitStatusForSubdir(m.workspaceRoot, relPath)
548-
if err == nil && (added > 0 || modified > 0 || deleted > 0) {
549-
res.changes = &GitChanges{Added: added, Modified: modified, Deleted: deleted}
550-
}
551-
}
552596
}
553597
}
554598

555-
// 2. Check NPM updates
599+
// 2. NPM latest version check
556600
if entry.Installed && !entry.LocalSource {
557601
localVer := getLocalVersion(m.workspaceRoot, entry)
558602
if localVer != "" {
@@ -572,9 +616,6 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
572616
var checkErr error
573617
for i := 0; i < len(m.catalog.Entries); i++ {
574618
res := <-resultsChan
575-
if res.changes != nil {
576-
gitChangesMap[res.shortName] = *res.changes
577-
}
578619
if res.update != nil {
579620
updatesMap[res.shortName] = *res.update
580621
}
@@ -583,7 +624,7 @@ func (m *TuiModel) checkForUpdatesCmd() tea.Cmd {
583624
}
584625
}
585626

586-
return updatesLoadedMsg{GitChanges: gitChangesMap, Updates: updatesMap, Err: checkErr}
627+
return remoteUpdatesMsg{Updates: updatesMap, Err: checkErr}
587628
}
588629
}
589630

go/tui/types.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,16 @@ type UpdateInfo struct {
6565
Npm bool
6666
}
6767

68-
type updatesLoadedMsg struct {
69-
GitChanges map[string]GitChanges
70-
Updates map[string]UpdateInfo
71-
Err error
68+
// localChangesMsg is the result of the fast local-only git status check (no network).
69+
type localChangesMsg struct {
70+
GitChanges map[string]GitChanges
71+
LocalGitDirs map[string]bool // shortName -> has .git folder
72+
}
73+
74+
// remoteUpdatesMsg is the result of the slow network check (git behind + npm versions).
75+
type remoteUpdatesMsg struct {
76+
Updates map[string]UpdateInfo
77+
Err error
7278
}
7379

7480
type setupProgressMsg struct {
@@ -179,6 +185,7 @@ type TuiModel struct {
179185

180186
gitChangesMap map[string]GitChanges
181187
updatesMap map[string]UpdateInfo
188+
localGitDirs map[string]bool // shortName -> has own .git folder
182189

183190
settingsSel int
184191
settingsInstallMode string

go/tui/view.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -379,20 +379,8 @@ func (m *TuiModel) renderCatalogRow(item bridge.CatalogEntry, selected bool, w,
379379

380380
source := "npm"
381381
if item.LocalSource {
382-
if m.ctx != nil && m.ctx.Settings.LastInstallChoices != nil {
383-
if choice, exists := m.ctx.Settings.LastInstallChoices[item.Name]; exists {
384-
if choiceMap, ok := choice.(map[string]interface{}); ok {
385-
if _, hasGit := choiceMap["gitUrl"].(string); hasGit {
386-
source = "git"
387-
} else {
388-
source = "dev"
389-
}
390-
} else {
391-
source = "dev"
392-
}
393-
} else {
394-
source = "dev"
395-
}
382+
if m.localGitDirs[item.ShortName] {
383+
source = "git"
396384
} else {
397385
source = "dev"
398386
}
@@ -419,7 +407,7 @@ func (m *TuiModel) renderCatalogRow(item bridge.CatalogEntry, selected bool, w,
419407
}
420408

421409
// Git behind indicator
422-
if upInfo, ok := m.updatesMap[item.Name]; ok {
410+
if upInfo, ok := m.updatesMap[item.ShortName]; ok {
423411
if upInfo.LocalGit && upInfo.BehindCount > 0 {
424412
status += warnStyle.Render(fmt.Sprintf(" (-%d)", upInfo.BehindCount))
425413
} else if upInfo.Npm {
@@ -428,7 +416,7 @@ func (m *TuiModel) renderCatalogRow(item bridge.CatalogEntry, selected bool, w,
428416
}
429417

430418
// Local edits indicator (Git dirty status)
431-
if gitStat, ok := m.gitChangesMap[item.Name]; ok {
419+
if gitStat, ok := m.gitChangesMap[item.ShortName]; ok {
432420
total := gitStat.Added + gitStat.Modified + gitStat.Deleted
433421
if total > 0 {
434422
status += dirtyIndicatorStyle.Render("*")

0 commit comments

Comments
 (0)