Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scanner/excluded.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ func (e Excluder) FilterPorcelainStatus(st PorcelainStatus) PorcelainStatus {
return filtered
}

func NewExcluder(files, dirs []string) (Excluder, error) {
func NewExcluder(files, dirs []string) Excluder {
return Excluder{
files: files,
dirs: dirs,
}, nil
}
}
10 changes: 5 additions & 5 deletions scanner/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ import (
"golang.org/x/sync/errgroup"
)

func skip(needle string, haystack []string) bool {
return slices.Contains(haystack, needle)
}

func isGitMetadataDir(path string, d fs.DirEntry) (bool, error) {
if d.Name() != ".git" {
return false, nil
Expand Down Expand Up @@ -60,7 +56,11 @@ func walkone(ctx context.Context, dir string, config *Config, results chan strin
default:
}

if skip(path, config.ScanDirs.Exclude) {
if d.IsDir() {
log.Printf("path %s", path)
}

if slices.Contains(config.ScanDirs.Exclude, path) {
return filepath.SkipDir
}

Expand Down
46 changes: 12 additions & 34 deletions scanner/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ func Scan(config *Config) (*MultiGitStatus, error) {
// ScanWithProgress runs the same scan as [Scan] and invokes onProgress from concurrent
// discovery and the status loop. Callbacks should be non-blocking (e.g. small channel send).
func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitStatus, error) {
ex, e := NewExcluder(config.GitIgnore.FileGlob, config.GitIgnore.DirGlob)
if e != nil {
return nil, e
}

ctx := context.Background()
repositories := make(chan string, 1000)

Expand Down Expand Up @@ -70,18 +65,10 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS
CurrentPath: d,
})

start := time.Now()

porcelain, err := GitStatus(d)
rs, include, err := StatusForRepo(config, d)
if err != nil {
return err
}
porcelain = ex.FilterPorcelainStatus(porcelain)
st := porcelain.ToGitStatus()

duration := time.Since(start)
log.Println(d, duration)

n := checked.Add(1)
// Git status finished for this repo; advance ReposChecked and retain
// CurrentPath until the next channel receive so the path line does not
Expand All @@ -91,20 +78,11 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS
ReposChecked: int(n),
CurrentPath: d,
})
log.Println(d, rs.ScanTime)

branches, err := GitBranchStatus(d)
if err != nil {
log.Printf("branch status scan failed for %s: %v", d, err)
}
branches.FilterLocalOnlyForConfig(config)
if !st.IsClean() || branches.HasLocalRemoteMismatch() {
atomic.AddInt64(&totalStatusDuration, duration.Nanoseconds())
results.AddResult(d, RepoStatus{
Status: st,
Porcelain: porcelain,
Branches: branches,
ScanTime: duration,
})
if include {
atomic.AddInt64(&totalStatusDuration, rs.ScanTime.Nanoseconds())
results.AddResult(d, rs)
}
return nil
})
Expand All @@ -123,11 +101,11 @@ func ScanWithProgress(config *Config, onProgress func(ScanProgress)) (*MultiGitS
// StatusForRepo returns fresh status for a single repository directory using the
// same porcelain filtering and branch metadata as [ScanWithProgress]. The bool
// is whether this repo should appear in the dirty list (!clean or remote mismatch).
// If ex is nil, a new excluder is built from config (for single-repo refresh paths).
// afterPorcelain, if non-nil, is invoked after porcelain is resolved and before
// branch metadata is collected (used by [ScanWithProgress] for progress timing).
func StatusForRepo(config *Config, dir string) (RepoStatus, bool, error) {
ex, err := NewExcluder(config.GitIgnore.FileGlob, config.GitIgnore.DirGlob)
if err != nil {
return RepoStatus{}, false, err
}
ex := NewExcluder(config.GitIgnore.FileGlob, config.GitIgnore.DirGlob)
start := time.Now()
porcelain, err := GitStatus(dir)
if err != nil {
Expand All @@ -136,9 +114,9 @@ func StatusForRepo(config *Config, dir string) (RepoStatus, bool, error) {
porcelain = ex.FilterPorcelainStatus(porcelain)
st := porcelain.ToGitStatus()

branches, berr := GitBranchStatus(dir)
if berr != nil {
log.Printf("branch status scan failed for %s: %v", dir, berr)
branches, err := GitBranchStatus(dir)
if err != nil {
log.Printf("branch status scan failed for %s: %v", dir, err)
}
branches.FilterLocalOnlyForConfig(config)

Expand Down
2 changes: 1 addition & 1 deletion ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Run(config *scanner.Config) error {

m := &model{
config: config,
logBuf: &logBuffer{max: 500},
logBuf: &logBuffer{max: 10000},
repositories: scanner.NewMultiGitStatus(),
focus: paneRepo,
scanResultCh: make(chan scanResult, 1),
Expand Down
173 changes: 94 additions & 79 deletions ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,97 @@ func (m *model) handleCommandKey(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) {
// listKeyScrollPage is the step size for Shift+↑/↓ in scrollable lists and viewports.
const listKeyScrollPage = 10

// viewportVerticalKey scrolls a viewport by a page (shift arrows) or delegates to Update for single-line motion.
func viewportVerticalKey(vp viewport.Model, msg tea.KeyMsg, step int, up bool) (viewport.Model, tea.Cmd) {
if step == listKeyScrollPage {
if up {
vp.ScrollUp(listKeyScrollPage)
} else {
vp.ScrollDown(listKeyScrollPage)
}
return vp, nil
}
return vp.Update(msg)
}

func (m *model) repoListVerticalArrow(step int, up, down bool) (tea.Model, tea.Cmd, bool) {
prev := m.cursor
if up && m.cursor > 0 {
m.cursor = max(0, m.cursor-step)
} else if down && len(m.repoList) > 0 && m.cursor < len(m.repoList)-1 {
m.cursor = min(len(m.repoList)-1, m.cursor+step)
}
if m.cursor != prev {
m.statusFileSelected = false
m.diffNeedsRefresh = true
m.syncRepoListScrollOnly()
m.repoNavSettleGen++
g := m.repoNavSettleGen
return m, tea.Tick(repoNavSettleDebounce, func(t time.Time) tea.Msg {
return repoNavSettledMsg{gen: g}
}), true
}
return m, nil, true
}

func (m *model) statusVerticalArrow(msg tea.KeyMsg, step int, up bool) (tea.Model, tea.Cmd, bool) {
if !m.statusFileSelected && len(m.statusPaths) > 0 {
m.statusFileSelected = true
m.statusTable.Focus()
}
var cmd tea.Cmd
if step == listKeyScrollPage {
if up {
m.statusTable.MoveUp(listKeyScrollPage)
} else {
m.statusTable.MoveDown(listKeyScrollPage)
}
} else {
m.statusTable, cmd = m.statusTable.Update(msg)
}
if len(m.statusPaths) > 0 {
m.statusFileSelected = true
m.diffNeedsRefresh = true
m.syncViewports()
}
return m, cmd, true
}

func (m *model) focusStatusDiff(toDiff bool) {
if toDiff {
m.focus = paneDiff
if m.zoomed {
m.zoomTarget = paneDiff
}
} else {
m.focus = paneStatus
if m.zoomed {
m.zoomTarget = paneStatus
}
}
m.syncViewports()
}

func (m *model) handleVerticalArrowKey(msg tea.KeyMsg, step int, up, down bool) (tea.Model, tea.Cmd, bool) {
switch m.focus {
case paneRepo:
return m.repoListVerticalArrow(step, up, down)
case paneStatus:
return m.statusVerticalArrow(msg, step, up)
case paneDiff:
var cmd tea.Cmd
m.diffVP, cmd = viewportVerticalKey(m.diffVP, msg, step, up)
return m, cmd, true
case paneLog:
m.setLogVPContent()
var cmd tea.Cmd
m.logVP, cmd = viewportVerticalKey(m.logVP, msg, step, up)
return m, cmd, true
default:
return m, nil, false
}
}

// handleArrowKey applies directional keys: scrolling / repo navigation, and
// ←/→ to move focus between Status and Diff.
func (m *model) handleArrowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) {
Expand All @@ -449,91 +540,15 @@ func (m *model) handleArrowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd, bool) {
}
up := msg.Type == tea.KeyUp || msg.Type == tea.KeyShiftUp
down := msg.Type == tea.KeyDown || msg.Type == tea.KeyShiftDown

if m.focus == paneRepo {
prev := m.cursor
if up && m.cursor > 0 {
m.cursor = max(0, m.cursor-step)
} else if down && len(m.repoList) > 0 && m.cursor < len(m.repoList)-1 {
m.cursor = min(len(m.repoList)-1, m.cursor+step)
}
if m.cursor != prev {
m.statusFileSelected = false
m.diffNeedsRefresh = true
m.syncRepoListScrollOnly()
m.repoNavSettleGen++
g := m.repoNavSettleGen
return m, tea.Tick(repoNavSettleDebounce, func(t time.Time) tea.Msg {
return repoNavSettledMsg{gen: g}
}), true
}
return m, nil, true
}
if m.focus == paneStatus {
if !m.statusFileSelected && len(m.statusPaths) > 0 {
m.statusFileSelected = true
m.statusTable.Focus()
}
var cmd tea.Cmd
if step == listKeyScrollPage {
if up {
m.statusTable.MoveUp(listKeyScrollPage)
} else {
m.statusTable.MoveDown(listKeyScrollPage)
}
} else {
m.statusTable, cmd = m.statusTable.Update(msg)
}
if len(m.statusPaths) > 0 {
m.statusFileSelected = true
m.diffNeedsRefresh = true
m.syncViewports()
}
return m, cmd, true
}
if m.focus == paneDiff {
if step == listKeyScrollPage {
if up {
m.diffVP.ScrollUp(listKeyScrollPage)
} else {
m.diffVP.ScrollDown(listKeyScrollPage)
}
return m, nil, true
}
var cmd tea.Cmd
m.diffVP, cmd = m.diffVP.Update(msg)
return m, cmd, true
}
if m.focus == paneLog {
m.setLogVPContent()
if step == listKeyScrollPage {
if up {
m.logVP.ScrollUp(listKeyScrollPage)
} else {
m.logVP.ScrollDown(listKeyScrollPage)
}
return m, nil, true
}
var cmd tea.Cmd
m.logVP, cmd = m.logVP.Update(msg)
return m, cmd, true
}
return m.handleVerticalArrowKey(msg, step, up, down)
case tea.KeyRight:
if m.focus == paneStatus {
m.focus = paneDiff
if m.zoomed {
m.zoomTarget = paneDiff
}
m.syncViewports()
m.focusStatusDiff(true)
return m, nil, true
}
case tea.KeyLeft:
if m.focus == paneDiff {
m.focus = paneStatus
if m.zoomed {
m.zoomTarget = paneStatus
}
m.syncViewports()
m.focusStatusDiff(false)
return m, nil, true
}
}
Expand Down
Loading