tofix 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tofix/__init__.py +4 -0
- tofix/cli.py +29 -0
- tofix/core.py +174 -0
- tofix-1.0.0.dist-info/METADATA +57 -0
- tofix-1.0.0.dist-info/RECORD +8 -0
- tofix-1.0.0.dist-info/WHEEL +4 -0
- tofix-1.0.0.dist-info/entry_points.txt +3 -0
- tofix-1.0.0.dist-info/licenses/LICENSE.md +19 -0
tofix/__init__.py
ADDED
tofix/cli.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
from tofix import Tofix, Format
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def app():
|
|
7
|
+
parser = ArgumentParser()
|
|
8
|
+
parser.add_argument(
|
|
9
|
+
"--format",
|
|
10
|
+
choices=[Format.HUMAN, Format.MACHINE, Format.JSON],
|
|
11
|
+
default=Format.HUMAN,
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument("--cached", action="store_true")
|
|
14
|
+
parser.add_argument("--unstaged", action="store_true")
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
|
|
17
|
+
todos = Tofix(unstaged=args.unstaged, cached=args.cached)
|
|
18
|
+
_, lines = todos.files_and_lines()
|
|
19
|
+
|
|
20
|
+
if os.isatty(1) and args.format == Format.HUMAN:
|
|
21
|
+
Tofix.human_format(lines)
|
|
22
|
+
elif not os.isatty(1) or args.format == Format.MACHINE:
|
|
23
|
+
Tofix.machine_format(lines)
|
|
24
|
+
elif args.format == Format.JSON:
|
|
25
|
+
Tofix.json_format(lines)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
app()
|
tofix/core.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implements the `todos` application. See `todos.Tofix` for more information.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import List, Optional, Any, Tuple, Dict
|
|
10
|
+
from git import Git
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _remove_nones(values: List[Optional[Any]]) -> List[Any]:
|
|
14
|
+
"""Remove all None values from a list."""
|
|
15
|
+
return [v for v in values if v is not None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Colors:
|
|
19
|
+
BOLD_GREEN = "\033[1;32m"
|
|
20
|
+
BOLD_YELLOW = "\033[1;33m"
|
|
21
|
+
RESET = "\033[0m"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Format(str, Enum):
|
|
25
|
+
"""Formatting options for stdout."""
|
|
26
|
+
|
|
27
|
+
HUMAN = "human"
|
|
28
|
+
MACHINE = "machine"
|
|
29
|
+
JSON = "json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Line:
|
|
34
|
+
"""Dataclass for a line containing the "todo" key."""
|
|
35
|
+
|
|
36
|
+
path: str
|
|
37
|
+
number: int
|
|
38
|
+
text: str
|
|
39
|
+
|
|
40
|
+
def to_dict(self):
|
|
41
|
+
"""Represent as a dict."""
|
|
42
|
+
return {"file": self.path, "line": self.number, "text": self.text}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Tofix:
|
|
46
|
+
"""Implements the `todos` application.
|
|
47
|
+
|
|
48
|
+
Exposes one method `files_and_lines` which returns a list of raw file paths and `Line`
|
|
49
|
+
objects for every line in the currently checked out branch that contains a specified
|
|
50
|
+
"todo" `key`.
|
|
51
|
+
|
|
52
|
+
Uses `gitpython` under the hood to run the necessary `git cherry` and `git diff`
|
|
53
|
+
commands.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
base_branch: str
|
|
57
|
+
key: str
|
|
58
|
+
cached: bool
|
|
59
|
+
unstaged: bool
|
|
60
|
+
g: Git
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
base_branch: str = "main",
|
|
65
|
+
key: str = "TODO",
|
|
66
|
+
cached: bool = False,
|
|
67
|
+
unstaged: bool = False,
|
|
68
|
+
):
|
|
69
|
+
self.base_branch = base_branch
|
|
70
|
+
self.key = key
|
|
71
|
+
self.cached = cached
|
|
72
|
+
self.unstaged = unstaged
|
|
73
|
+
self.g = Git(os.getcwd())
|
|
74
|
+
|
|
75
|
+
if self.cached and self.unstaged:
|
|
76
|
+
raise ValueError("cached and unstaged are mutually exclusive options")
|
|
77
|
+
|
|
78
|
+
def _commits(self) -> List[str]:
|
|
79
|
+
# cherry outputs each commit that's in one branch but not the other
|
|
80
|
+
cherry_output = self.g.cherry(["-v", self.base_branch])
|
|
81
|
+
all_cherries = cherry_output.split("\n")
|
|
82
|
+
|
|
83
|
+
# git diff appends a "+ " in front of commits in the current branch that are not
|
|
84
|
+
# in the base branch"
|
|
85
|
+
added_cherries = [c for c in all_cherries if c[:2] == "+ "]
|
|
86
|
+
|
|
87
|
+
# there's a space between the "+" and the commit hash
|
|
88
|
+
hashes = [c.split(" ")[1] for c in added_cherries]
|
|
89
|
+
return hashes
|
|
90
|
+
|
|
91
|
+
def _files(self, left: str, right: Optional[str] = None) -> List[str]:
|
|
92
|
+
# Get names of files that changed in this commit range that have the key in them.
|
|
93
|
+
flags = ["--name-only", "-S", self.key]
|
|
94
|
+
if self.cached:
|
|
95
|
+
flags.append("--cached")
|
|
96
|
+
diff_args = _remove_nones([*flags, left, right])
|
|
97
|
+
|
|
98
|
+
diff_output = self.g.diff(diff_args)
|
|
99
|
+
files_with_keys = [line for line in diff_output.split("\n") if line != ""]
|
|
100
|
+
return files_with_keys
|
|
101
|
+
|
|
102
|
+
def _files_and_lines(
|
|
103
|
+
self, left: str, right: Optional[str]
|
|
104
|
+
) -> Tuple[List[str], List[Line]]:
|
|
105
|
+
flags = ["-U999999"]
|
|
106
|
+
if self.cached:
|
|
107
|
+
flags.append("--cached")
|
|
108
|
+
|
|
109
|
+
paths = self._files(left, right)
|
|
110
|
+
result: List[Line] = []
|
|
111
|
+
|
|
112
|
+
for path in paths:
|
|
113
|
+
diff_args = _remove_nones([*flags, left, right, "--", path])
|
|
114
|
+
diff_output = self.g.diff(diff_args)
|
|
115
|
+
|
|
116
|
+
# ignore the first 6 lines, which display metadata
|
|
117
|
+
diff_lines = diff_output.split("\n")[6:]
|
|
118
|
+
|
|
119
|
+
# we need to figure out what the line number is for each added line
|
|
120
|
+
lines_with_key = [
|
|
121
|
+
Line(path=path, number=number, text=text[1:])
|
|
122
|
+
for number, text in enumerate(diff_lines)
|
|
123
|
+
if text[:1] == "+" and self.key in text
|
|
124
|
+
]
|
|
125
|
+
result += lines_with_key
|
|
126
|
+
|
|
127
|
+
return paths, result
|
|
128
|
+
|
|
129
|
+
def files_and_lines(self) -> Tuple[List[str], List[Line]]:
|
|
130
|
+
"""Return a list of `Line` objects for added line containing `self.key`."""
|
|
131
|
+
commits = self._commits()
|
|
132
|
+
|
|
133
|
+
# On the base branch there are no added commits, so nothing was introduced here.
|
|
134
|
+
if not commits:
|
|
135
|
+
return [], []
|
|
136
|
+
|
|
137
|
+
# We want to diff with the commit *before* the first one we added in our branch
|
|
138
|
+
left = f"{commits[0]}^"
|
|
139
|
+
|
|
140
|
+
# The right side of the diff should be empty unless we're only looking at
|
|
141
|
+
# committed files
|
|
142
|
+
right = None if self.cached or self.unstaged else commits[-1]
|
|
143
|
+
|
|
144
|
+
return self._files_and_lines(left, right)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _lines_by_path(lines: List[Line]) -> Dict[str, List[Line]]:
|
|
148
|
+
mapping = {}
|
|
149
|
+
for line in lines:
|
|
150
|
+
if mapping.get(line.path) is None:
|
|
151
|
+
mapping[line.path] = []
|
|
152
|
+
mapping[line.path].append(line)
|
|
153
|
+
return mapping
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def human_format(cls, lines: List[Line]):
|
|
157
|
+
"""Print the lines in a format comparable to that of `ack` and `ag`."""
|
|
158
|
+
for path, _lines in Tofix._lines_by_path(lines).items():
|
|
159
|
+
print(f"{Colors.BOLD_GREEN}{path}{Colors.RESET}")
|
|
160
|
+
for line in _lines:
|
|
161
|
+
print(f"{Colors.BOLD_YELLOW}{line.number}{Colors.RESET}: {line.text}")
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def machine_format(cls, lines: List[Line]):
|
|
165
|
+
"""Print the lines in a format consumable by `fpp`."""
|
|
166
|
+
for line in lines:
|
|
167
|
+
print(f"{line.path}:{line.number}:{line.text}")
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def json_format(cls, lines: List[Line]):
|
|
171
|
+
"""Print the lines as a json array to stdout."""
|
|
172
|
+
objects = [line.to_dict() for line in lines]
|
|
173
|
+
output = json.dumps(objects, indent=2)
|
|
174
|
+
print(output)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tofix
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Find TODO and FIXME comments introduced on the current branch vs a base branch.
|
|
5
|
+
Keywords: fixme,git,task,todo
|
|
6
|
+
Author: Piper Maxine Baker
|
|
7
|
+
Author-email: Piper Maxine Baker <dev@piper.community>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Requires-Dist: gitpython>=3.1
|
|
11
|
+
Maintainer: Piper Maxine Baker
|
|
12
|
+
Maintainer-email: Piper Maxine Baker <dev@piper.community>
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# `tofix`
|
|
17
|
+
|
|
18
|
+
A tool to find all `TODO` and `FIXME` comments introduced on your dev branch. No more
|
|
19
|
+
polluting your task list with those left by previous developers in the codebase. Just the
|
|
20
|
+
changes *you* made.
|
|
21
|
+
|
|
22
|
+
## usage
|
|
23
|
+
|
|
24
|
+
Run `tofix` anywhere in a git repository while on a non-`main` branch. If outputting
|
|
25
|
+
to a terminal, it gives a nice, human-readable format.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
> tofix
|
|
29
|
+
README.md
|
|
30
|
+
7:TODO write an intro
|
|
31
|
+
8:TODO write an outro
|
|
32
|
+
todos.sh
|
|
33
|
+
48:for file in $(git diff --name-only -S"TODO" "${earliest}^" $latest); do
|
|
34
|
+
52: | grep "TODO" \
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If piped to another command (e.g., [`fpp`](https://github.com/facebook/PathPicker)), it
|
|
38
|
+
uses a machine-readable format:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
> tofix | fpp
|
|
42
|
+
README.md:7:TODO write an intro
|
|
43
|
+
README.md:8:TODO write an outro
|
|
44
|
+
todos.sh:48:for file in $(git diff --name-only -S"TODO" "${earliest}^" $latest); do
|
|
45
|
+
todos.sh:52: | grep "TODO" \
|
|
46
|
+
|
|
47
|
+
________________________________________________________________________________________________________
|
|
48
|
+
[f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick select mode, [c] command mode
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both of these formats are inspired by the output of
|
|
52
|
+
[`ag`](https://github.com/mizuno-as/silversearcher-ag).
|
|
53
|
+
|
|
54
|
+
## future work
|
|
55
|
+
|
|
56
|
+
- [ ] `emacs` integration
|
|
57
|
+
- [ ] `vscode` integration
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
tofix/__init__.py,sha256=7tkiTxRC0UNWR7Xe5Ny-plyrL-kVTiiC8FRaUeOPAm4,115
|
|
2
|
+
tofix/cli.py,sha256=GtI7Xfm-QCzRA-FSSU5xs_kZO68aV5hdGS3yXrwZkns,805
|
|
3
|
+
tofix/core.py,sha256=oPIU6Qbkebbfpd1Ps30n9K2kUXnYunl_2qinU62tx7k,5552
|
|
4
|
+
tofix-1.0.0.dist-info/licenses/LICENSE.md,sha256=YVFy62PThcARSS5uQ3BVIZ7xnysxTxIsEC6-iP1QvvI,1090
|
|
5
|
+
tofix-1.0.0.dist-info/WHEEL,sha256=jK0lbM7sVtq70msNoYotEXYS3OJMDdns2CRgyjhimnE,81
|
|
6
|
+
tofix-1.0.0.dist-info/entry_points.txt,sha256=XD2wLLxe2a6h_SkVT0salPuVraazvn2GC9I3ROvAXCY,41
|
|
7
|
+
tofix-1.0.0.dist-info/METADATA,sha256=xATQ40gQSGVJ8MfMW7xFF9cgijXWRKvyyF8hriUZ8Cc,1735
|
|
8
|
+
tofix-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Piper Maxine Baker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
9
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
15
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
16
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
17
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
18
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
DEALINGS IN THE SOFTWARE.
|