git: a creation gate that never writes history, a git tab with Paperclip's worktree scan, and a warning line in every pane
This commit is contained in:
180
tui/project.go
180
tui/project.go
@@ -21,6 +21,8 @@ import (
|
||||
)
|
||||
|
||||
type projectMsg struct {
|
||||
git gitState
|
||||
wts []worktreeRow
|
||||
dash paperclip.Dashboard
|
||||
project paperclip.Project
|
||||
cfg map[string]string
|
||||
@@ -50,6 +52,9 @@ type projectModel struct {
|
||||
err error
|
||||
readAt time.Time
|
||||
|
||||
git gitState
|
||||
wts []worktreeRow
|
||||
|
||||
tab int
|
||||
scroll int
|
||||
regAt int
|
||||
@@ -60,7 +65,18 @@ type projectModel struct {
|
||||
width, height int
|
||||
}
|
||||
|
||||
var projectTabs = []string{"overview", "facts", "costs", "settings", "projects"}
|
||||
// Tab order is also the reading order, and the numbers are named because they
|
||||
// are used in the key handling: a bare 4 broke once already.
|
||||
const (
|
||||
tabOverview = iota
|
||||
tabGit
|
||||
tabFacts
|
||||
tabCosts
|
||||
tabSettings
|
||||
tabProjects
|
||||
)
|
||||
|
||||
var projectTabs = []string{"overview", "git", "facts", "costs", "settings", "projects"}
|
||||
|
||||
func newProjectModel(c *paperclip.Client, folder string) projectModel {
|
||||
return projectModel{client: c, folder: folder}
|
||||
@@ -104,7 +120,10 @@ func (m projectModel) load() tea.Cmd {
|
||||
costs = append(costs, taskCost{t.Identifier, t.Status, cost.CostCents, cost.RunCount})
|
||||
}
|
||||
dash, _ := c.DashboardSummary()
|
||||
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs, dash: dash}
|
||||
git := readGitState(folder)
|
||||
wts, _ := readWorktrees(c, proj.ID, 8)
|
||||
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs,
|
||||
dash: dash, git: git, wts: wts}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +203,13 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Quit
|
||||
case "r":
|
||||
return m, m.load()
|
||||
case "t":
|
||||
// lazygit, in the project folder: it is the tool that can stage,
|
||||
// exclude and commit. Nothing here reimplements it.
|
||||
if err := openTool(m.folder, "lazygit"); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
return m, nil
|
||||
case "R":
|
||||
if err := startRebuild(m.folder); err != nil {
|
||||
m.err = err
|
||||
@@ -191,7 +217,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
if m.tab == 4 {
|
||||
if m.tab == tabProjects {
|
||||
m.regs = readRegistry()
|
||||
if len(m.regs) > 0 && m.regAt < len(m.regs) {
|
||||
m.confirmSwitch = true
|
||||
@@ -211,13 +237,13 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.tab == 4 && m.regAt > 0 {
|
||||
if m.tab == tabProjects && m.regAt > 0 {
|
||||
m.regAt--
|
||||
}
|
||||
m.scroll--
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
if m.tab == 4 && m.regAt < len(m.regs)-1 {
|
||||
if m.tab == tabProjects && m.regAt < len(m.regs)-1 {
|
||||
m.regAt++
|
||||
}
|
||||
m.scroll++
|
||||
@@ -234,6 +260,8 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.cfg = msg.cfg
|
||||
m.agents = msg.agents
|
||||
m.costs = msg.costs
|
||||
m.git = msg.git
|
||||
m.wts = msg.wts
|
||||
m.err = msg.err
|
||||
m.readAt = time.Now()
|
||||
return m, nil
|
||||
@@ -263,7 +291,9 @@ func (m projectModel) View() string {
|
||||
}
|
||||
if m.showHelp {
|
||||
return helpOverlay("the project", [][2]string{
|
||||
{"[ ] h l", "facts, costs, settings"},
|
||||
{"( )", "overview, git, facts, costs, settings, projects"},
|
||||
{"t", "open lazygit in this folder"},
|
||||
{"[ ] h l", "previous and next tab"},
|
||||
{"↑ ↓ j k", "scroll"},
|
||||
{"r", "read again"},
|
||||
{"R", "rebuild every pane"},
|
||||
@@ -283,13 +313,15 @@ func (m projectModel) View() string {
|
||||
|
||||
var body []string
|
||||
switch m.tab {
|
||||
case 0:
|
||||
case tabOverview:
|
||||
body = m.overviewLines(m.width - 4)
|
||||
case 1:
|
||||
case tabGit:
|
||||
body = m.gitLines(m.width - 4)
|
||||
case tabFacts:
|
||||
body = m.factLines()
|
||||
case 2:
|
||||
case tabCosts:
|
||||
body = m.costLines()
|
||||
case 4:
|
||||
case tabProjects:
|
||||
body = m.projectListLines(m.width - 4)
|
||||
default:
|
||||
body = m.settingLines()
|
||||
@@ -312,6 +344,7 @@ func (m projectModel) View() string {
|
||||
|
||||
footer := hintLine([]string{
|
||||
keyHint("( )", "tab"),
|
||||
keyHint("t", "lazygit"),
|
||||
keyHint("↑↓", "scroll"),
|
||||
keyHint("r", "refresh"),
|
||||
keyHint("R", "rebuild"),
|
||||
@@ -574,3 +607,130 @@ func (m projectModel) projectListLines(width int) []string {
|
||||
out = append(out, dimStyle().Render(" ~/.config/tuios hosts table; otherwise attach to it by hand."))
|
||||
return out
|
||||
}
|
||||
|
||||
// gitLines is the git tab: the folder read locally, then what Paperclip knows
|
||||
// about each task's worktree. The two halves answer different questions and are
|
||||
// not merged into one verdict.
|
||||
func (m projectModel) gitLines(width int) []string {
|
||||
var out []string
|
||||
g := m.git
|
||||
|
||||
report := func(label, value string, bad bool) {
|
||||
line := " " + mutedStyle2().Render(fmt.Sprintf("%-14s", label))
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text))
|
||||
if bad {
|
||||
style = style.Foreground(lipgloss.Color(active.Warning))
|
||||
}
|
||||
out = append(out, line+style.Render(value))
|
||||
}
|
||||
|
||||
// --- the folder ---
|
||||
out = append(out, mutedStyle2().Render(" THIS FOLDER"))
|
||||
if !g.IsRepo {
|
||||
report("repository", "no — this folder is not a git repository", true)
|
||||
} else {
|
||||
branch := g.Branch
|
||||
if branch == "" {
|
||||
branch = "(no branch yet)"
|
||||
}
|
||||
report("repository", "yes · branch "+branch, false)
|
||||
report("commits", fmt.Sprintf("%d", g.Commits), !g.HasCommit)
|
||||
if g.Commits == 0 {
|
||||
out = append(out, " no commit, so nothing to cut a worktree from")
|
||||
}
|
||||
remotes := "none — not needed for worktrees"
|
||||
if len(g.Remotes) > 0 {
|
||||
remotes = strings.Join(g.Remotes, ", ")
|
||||
}
|
||||
report("remote", remotes, false)
|
||||
report("changed", fmt.Sprintf("%d staged · %d unstaged · %d untracked",
|
||||
g.Staged, g.Unstaged, g.Untracked), false)
|
||||
if g.HasIgnore {
|
||||
shown := strings.Join(g.Ignore, " ")
|
||||
if len(shown) > width-24 {
|
||||
shown = shown[:width-25] + "…"
|
||||
}
|
||||
if shown == "" {
|
||||
shown = "(no entries)"
|
||||
}
|
||||
report(".gitignore", shown, !g.ignoresPaperclipWorktrees())
|
||||
} else {
|
||||
report(".gitignore", "missing — .paperclip/ must be in it", true)
|
||||
}
|
||||
}
|
||||
out = append(out, "")
|
||||
|
||||
// --- what Paperclip will do with each task ---
|
||||
policy := "not set"
|
||||
if p := m.project.ExecutionWorkspacePolicy; p != nil && p.Enabled {
|
||||
policy = p.DefaultMode
|
||||
if p.Strategy.Type != "" {
|
||||
policy += " · " + p.Strategy.Type
|
||||
}
|
||||
if p.Strategy.Base != "" {
|
||||
policy += " · base " + p.Strategy.Base
|
||||
}
|
||||
}
|
||||
report("worktrees", policy, m.project.ExecutionWorkspacePolicy == nil ||
|
||||
(m.project.ExecutionWorkspacePolicy != nil && m.project.ExecutionWorkspacePolicy.Strategy.Type != "git_worktree"))
|
||||
out = append(out, "")
|
||||
|
||||
// --- what to do, when something is wrong ---
|
||||
if !g.ready() || !g.ignoresPaperclipWorktrees() {
|
||||
out = append(out, mutedStyle2().Render(" WHAT TO DO"))
|
||||
folder := m.folder
|
||||
if !g.ignoresPaperclipWorktrees() {
|
||||
out = append(out, " .paperclip/ is not ignored: a commit would swallow every worktree.")
|
||||
out = append(out, " "+dimStyle().Render("t lazygit · select .gitignore · e to edit · add .paperclip/"))
|
||||
out = append(out, "")
|
||||
}
|
||||
if !g.IsRepo {
|
||||
out = append(out, " This folder is not a repository, so no task can have its own branch.")
|
||||
out = append(out, " "+dimStyle().Render("cd "+folder+" && git init -b main"))
|
||||
out = append(out, " "+dimStyle().Render("then commit something — in lazygit: t, then c"))
|
||||
} else if !g.HasCommit {
|
||||
out = append(out, " This repository has no commit, so there is nothing to cut a worktree from.")
|
||||
out = append(out, " "+dimStyle().Render("t lazygit · space to stage · c to commit"))
|
||||
}
|
||||
out = append(out, "")
|
||||
out = append(out, " Then run "+keyHint("R", "rebuild")+" so every pane is built against the new state.")
|
||||
out = append(out, "")
|
||||
}
|
||||
|
||||
// --- Paperclip's own scan of the task worktrees ---
|
||||
out = append(out, mutedStyle2().Render(" WORKTREES, AS PAPERCLIP SEES THEM"))
|
||||
if len(m.wts) == 0 {
|
||||
out = append(out, dimStyle().Render(" none yet — one is made when a task runs"))
|
||||
}
|
||||
for _, w := range m.wts {
|
||||
state := "clean"
|
||||
if w.Dirty > 0 {
|
||||
state = fmt.Sprintf("%d changed", w.Dirty)
|
||||
}
|
||||
extra := ""
|
||||
if w.Ahead > 0 {
|
||||
extra += fmt.Sprintf(" · %d ahead", w.Ahead)
|
||||
}
|
||||
if w.Behind > 0 {
|
||||
extra += fmt.Sprintf(" · %d behind", w.Behind)
|
||||
}
|
||||
if w.Merged {
|
||||
extra += " · merged into base"
|
||||
}
|
||||
branch := w.Branch
|
||||
if branch == "" {
|
||||
branch = "(no branch)"
|
||||
}
|
||||
if len(branch) > width-34 {
|
||||
branch = branch[:width-35] + "…"
|
||||
}
|
||||
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(branch)+
|
||||
dimStyle().Render(" "+state+extra))
|
||||
if w.Warning != "" {
|
||||
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Warning)).Render(w.Warning))
|
||||
}
|
||||
}
|
||||
out = append(out, "")
|
||||
out = append(out, dimStyle().Render(" read from git in this folder, and from Paperclip's scan of each worktree"))
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user