Coverage for src/devboard/default_board.py: 0.00%
97 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 20:21 +0100
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 20:21 +0100
1"""User configuration of columns.
3The only object that must be defined in this module is `columns`,
4which is a list of `Column` instances.
5"""
7from __future__ import annotations
9import os
10from pathlib import Path
11from typing import Any, ClassVar, Iterator
13from git import GitCommandError
15from devboard.board import Column, Row
16from devboard.projects import Project as BaseProject
18BASE_DIR = Path(os.getenv("DEVBOARD_PROJECTS", Path.home() / "dev")).expanduser()
19"""The base directory containing all your Git projects.
21This variable is only used to list projects in `Project.list_projects`
22and has no special meaning for Devboard.
23"""
26class Project(BaseProject):
27 """Customized project class.
29 The original `Project` is sub-classed for demonstration purpose.
30 Feel free to add any attribute, property or method to it,
31 to serve your own needs. You can also override its existing
32 property and methods if needed. In the default class below,
33 we add the `list_projects` class method that will be passed
34 to `Column` instances, allowing them to iterate on your projects.
35 """
37 @classmethod
38 def list_projects(cls) -> Iterator[Project]:
39 """List all Git projects in a base directory."""
40 for filedir in BASE_DIR.iterdir():
41 if filedir.is_dir() and filedir.joinpath(".git").is_dir():
42 yield cls(filedir)
45class ToCommit(Column):
46 """A column showing projects with uncommitted changes."""
48 TITLE = "To Commit"
49 HEADERS = ("Project", "Details")
50 THREADED = False
51 BINDINGS: ClassVar = [
52 ("s", "apply('status')", "Show status"),
53 ("d", "apply('diff')", "Show diff"),
54 ]
56 def list_projects(self) -> Iterator[Project]:
57 """List projects for this column."""
58 yield from Project.list_projects()
60 @staticmethod
61 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override]
62 """Scan a project, feeding rows to the table.
64 It returns a single row with the project and its status line.
65 """
66 return [(project, project.status_line)] if project.is_dirty else []
68 def apply(self, action: str, row: Row) -> None:
69 """Process actions.
71 It handles two actions: `status` and `diff`.
73 - `status`: Show the Git status of the selected project in a modal window
74 - `diff`: Show the Git diff of the selected project in a modal window.
75 """
76 if action == "status":
77 self.modal(text=row.project.repo.git(c="color.status=always").status())
78 if action == "diff":
79 self.modal(text=row.project.repo.git(c="color.ui=always").diff())
80 raise ValueError(f"Unknown action '{action}'")
83class ToPull(Column):
84 """A column showing branches with commits that should be pulled."""
86 TITLE = "To Pull"
87 HEADERS = ("Project", "Branch", "Commits")
88 BINDINGS: ClassVar = [
89 ("p", "apply('pull')", "Pull"),
90 ("d", "apply('delete')", "Delete branch"),
91 ]
93 def list_projects(self) -> Iterator[Project]:
94 """List projects for this column."""
95 yield from Project.list_projects()
97 @staticmethod
98 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override]
99 """Scan a project, feeding rows to the table.
101 It returns multiple rows, one for each branch having commits to pull from the remote.
102 """
103 return [(project, branch, commits) for branch, commits in project.unpulled().items() if commits]
105 def apply(self, action: str, row: Row) -> None: # noqa: ARG002
106 """Process actions.
108 It handles a single default action: running `git pull` for the selected row
109 (project and branch).
110 """
111 project, branch, _ = row.data
112 message = f"Pulling branch [i]{branch}[/] in [i]{project}[/]"
113 if not project.lock():
114 self.notify_warning(f"Prevented: {message}: An operation is ongoing")
115 return
116 if not project.is_dirty:
117 self.notify_info(f"Started: {message}")
118 try:
119 project.pull(branch)
120 except GitCommandError as error:
121 self.notify_error(f"{message}: {error}", timeout=10)
122 else:
123 self.notify_success(f"Finished: {message}")
124 row.remove()
125 else:
126 self.notify_warning(f"Prevented: {message}: project is dirty")
127 project.unlock()
130class ToPush(Column):
131 """A column showing branches with commits that should be pushed."""
133 TITLE = "To Push"
134 HEADERS = ("Project", "Branch", "Commits")
135 BINDINGS: ClassVar = [
136 ("p", "apply('push')", "Push"),
137 ]
139 def list_projects(self) -> Iterator[Project]:
140 """List projects for this column."""
141 yield from Project.list_projects()
143 @staticmethod
144 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override]
145 """Scan a project, feeding rows to the table.
147 It returns multiple rows, one for each branch having commits to push to the remote.
148 """
149 return [(project, branch, commits) for branch, commits in project.unpushed().items() if commits]
151 def apply(self, action: str, row: Row) -> None: # noqa: ARG002
152 """Process actions.
154 It handles a single default action: running `git push` for the selected row
155 (project and branch).
156 """
157 project, branch, _ = row.data
158 message = f"Pushing branch [i]{branch}[/] in [i]{project}[/]"
159 if not project.lock():
160 self.notify_warning(f"Prevented: {message}: An operation is ongoing")
161 return
162 self.notify_info(f"Started: {message}")
163 try:
164 project.push(branch)
165 except GitCommandError as error:
166 self.notify_error(f"{message}: {error}", timeout=10)
167 else:
168 self.notify_success(f"Finished: {message}")
169 row.remove()
170 project.unlock()
173class ToRelease(Column):
174 """A column showing projects with commits that should be released."""
176 TITLE = "To Release"
177 HEADERS = ("Project", "Details")
179 def list_projects(self) -> Iterator[Project]:
180 """List projects for this column."""
181 yield from Project.list_projects()
183 @staticmethod
184 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override]
185 """Scan a project, feeding rows to the table.
187 It returns a single row with the project and a summary of commit types.
188 """
189 commit_types = {"feat": "F", "fix": "X", "refactor": "R", "build": "B", "deps": "D"}
190 by_type = {commit_type: 0 for commit_type in commit_types}
191 for commit in project.unreleased():
192 for commit_type in commit_types:
193 if commit.summary.startswith(f"{commit_type}:"):
194 by_type[commit_type] += 1
195 parts = [f"{by_type[ct]}{commit_types[ct]}" for ct in commit_types if by_type[ct]]
196 if parts:
197 return [(project, " ".join(parts))]
198 return []
201columns = [
202 ToCommit,
203 ToPull,
204 ToPush,
205 ToRelease,
206]