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

1"""User configuration of columns. 

2 

3The only object that must be defined in this module is `columns`, 

4which is a list of `Column` instances. 

5""" 

6 

7from __future__ import annotations 

8 

9import os 

10from pathlib import Path 

11from typing import Any, ClassVar, Iterator 

12 

13from git import GitCommandError 

14 

15from devboard.board import Column, Row 

16from devboard.projects import Project as BaseProject 

17 

18BASE_DIR = Path(os.getenv("DEVBOARD_PROJECTS", Path.home() / "dev")).expanduser() 

19"""The base directory containing all your Git projects. 

20 

21This variable is only used to list projects in `Project.list_projects` 

22and has no special meaning for Devboard. 

23""" 

24 

25 

26class Project(BaseProject): 

27 """Customized project class. 

28 

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 """ 

36 

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) 

43 

44 

45class ToCommit(Column): 

46 """A column showing projects with uncommitted changes.""" 

47 

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 ] 

55 

56 def list_projects(self) -> Iterator[Project]: 

57 """List projects for this column.""" 

58 yield from Project.list_projects() 

59 

60 @staticmethod 

61 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override] 

62 """Scan a project, feeding rows to the table. 

63 

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 [] 

67 

68 def apply(self, action: str, row: Row) -> None: 

69 """Process actions. 

70 

71 It handles two actions: `status` and `diff`. 

72 

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}'") 

81 

82 

83class ToPull(Column): 

84 """A column showing branches with commits that should be pulled.""" 

85 

86 TITLE = "To Pull" 

87 HEADERS = ("Project", "Branch", "Commits") 

88 BINDINGS: ClassVar = [ 

89 ("p", "apply('pull')", "Pull"), 

90 ("d", "apply('delete')", "Delete branch"), 

91 ] 

92 

93 def list_projects(self) -> Iterator[Project]: 

94 """List projects for this column.""" 

95 yield from Project.list_projects() 

96 

97 @staticmethod 

98 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override] 

99 """Scan a project, feeding rows to the table. 

100 

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] 

104 

105 def apply(self, action: str, row: Row) -> None: # noqa: ARG002 

106 """Process actions. 

107 

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() 

128 

129 

130class ToPush(Column): 

131 """A column showing branches with commits that should be pushed.""" 

132 

133 TITLE = "To Push" 

134 HEADERS = ("Project", "Branch", "Commits") 

135 BINDINGS: ClassVar = [ 

136 ("p", "apply('push')", "Push"), 

137 ] 

138 

139 def list_projects(self) -> Iterator[Project]: 

140 """List projects for this column.""" 

141 yield from Project.list_projects() 

142 

143 @staticmethod 

144 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override] 

145 """Scan a project, feeding rows to the table. 

146 

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] 

150 

151 def apply(self, action: str, row: Row) -> None: # noqa: ARG002 

152 """Process actions. 

153 

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() 

171 

172 

173class ToRelease(Column): 

174 """A column showing projects with commits that should be released.""" 

175 

176 TITLE = "To Release" 

177 HEADERS = ("Project", "Details") 

178 

179 def list_projects(self) -> Iterator[Project]: 

180 """List projects for this column.""" 

181 yield from Project.list_projects() 

182 

183 @staticmethod 

184 def populate_rows(project: Project) -> list[tuple[Any, ...]]: # type: ignore[override] 

185 """Scan a project, feeding rows to the table. 

186 

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 [] 

199 

200 

201columns = [ 

202 ToCommit, 

203 ToPull, 

204 ToPush, 

205 ToRelease, 

206]