fixdoc 0.0.1__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.
- fixdoc/__init__.py +8 -0
- fixdoc/cli.py +26 -0
- fixdoc/commands/__init__.py +11 -0
- fixdoc/commands/analyze.py +313 -0
- fixdoc/commands/capture.py +109 -0
- fixdoc/commands/capture_handlers.py +298 -0
- fixdoc/commands/delete.py +72 -0
- fixdoc/commands/edit.py +118 -0
- fixdoc/commands/manage.py +67 -0
- fixdoc/commands/search.py +65 -0
- fixdoc/commands/sync.py +268 -0
- fixdoc/config.py +113 -0
- fixdoc/fix.py +19 -0
- fixdoc/formatter.py +62 -0
- fixdoc/git.py +263 -0
- fixdoc/markdown_parser.py +106 -0
- fixdoc/models.py +83 -0
- fixdoc/parsers/__init__.py +24 -0
- fixdoc/parsers/base.py +131 -0
- fixdoc/parsers/kubernetes.py +584 -0
- fixdoc/parsers/router.py +160 -0
- fixdoc/parsers/terraform.py +409 -0
- fixdoc/storage.py +146 -0
- fixdoc/sync_engine.py +330 -0
- fixdoc/terraform_parser.py +135 -0
- fixdoc-0.0.1.dist-info/METADATA +261 -0
- fixdoc-0.0.1.dist-info/RECORD +30 -0
- fixdoc-0.0.1.dist-info/WHEEL +5 -0
- fixdoc-0.0.1.dist-info/entry_points.txt +2 -0
- fixdoc-0.0.1.dist-info/top_level.txt +1 -0
fixdoc/commands/sync.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Sync commands for sharing fixes via Git."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ..config import ConfigManager
|
|
9
|
+
from ..git import GitOperations, GitError, SyncStatus
|
|
10
|
+
from ..storage import FixRepository
|
|
11
|
+
from ..sync_engine import SyncEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_sync_context():
|
|
15
|
+
"""Get shared objects for sync commands."""
|
|
16
|
+
base_path = Path.home() / ".fixdoc"
|
|
17
|
+
return {
|
|
18
|
+
"repo": FixRepository(base_path),
|
|
19
|
+
"config_manager": ConfigManager(base_path),
|
|
20
|
+
"git": GitOperations(base_path),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
def sync():
|
|
26
|
+
"""Sync fixes with a shared Git repository."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@sync.command()
|
|
31
|
+
@click.argument("repo_url")
|
|
32
|
+
@click.option("--branch", "-b", default="main", help="Branch to sync with")
|
|
33
|
+
@click.option("--name", "-n", default=None, help="Your name for attribution")
|
|
34
|
+
@click.option("--email", "-e", default=None, help="Your email for attribution")
|
|
35
|
+
def init(repo_url: str, branch: str, name: Optional[str], email: Optional[str]):
|
|
36
|
+
"""
|
|
37
|
+
Initialize sync with a shared Git repository.
|
|
38
|
+
|
|
39
|
+
\b
|
|
40
|
+
Example:
|
|
41
|
+
fixdoc sync init git@github.com:mycompany/infra-fixes.git
|
|
42
|
+
fixdoc sync init https://github.com/mycompany/infra-fixes.git
|
|
43
|
+
"""
|
|
44
|
+
ctx = get_sync_context()
|
|
45
|
+
config_manager = ctx["config_manager"]
|
|
46
|
+
git = ctx["git"]
|
|
47
|
+
|
|
48
|
+
config = config_manager.load()
|
|
49
|
+
if config.sync.remote_url:
|
|
50
|
+
if not click.confirm(
|
|
51
|
+
f"Sync already configured with {config.sync.remote_url}. Reconfigure?"
|
|
52
|
+
):
|
|
53
|
+
click.echo("Aborted.")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if not name:
|
|
57
|
+
name = click.prompt("Your name (for attribution)")
|
|
58
|
+
if not email:
|
|
59
|
+
email = click.prompt("Your email")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if not git.is_git_repo():
|
|
63
|
+
click.echo("Initializing Git repository...")
|
|
64
|
+
git.init()
|
|
65
|
+
|
|
66
|
+
if git.has_remote("origin"):
|
|
67
|
+
current_url = git.remote_get_url("origin")
|
|
68
|
+
if current_url != repo_url:
|
|
69
|
+
click.echo(f"Updating remote URL from {current_url} to {repo_url}")
|
|
70
|
+
git.remote_set_url("origin", repo_url)
|
|
71
|
+
else:
|
|
72
|
+
click.echo(f"Adding remote: {repo_url}")
|
|
73
|
+
git.remote_add("origin", repo_url)
|
|
74
|
+
|
|
75
|
+
config.sync.remote_url = repo_url
|
|
76
|
+
config.sync.branch = branch
|
|
77
|
+
config.user.name = name
|
|
78
|
+
config.user.email = email
|
|
79
|
+
config_manager.save(config)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
click.echo(f"Fetching from remote...")
|
|
83
|
+
git.fetch("origin")
|
|
84
|
+
|
|
85
|
+
status = git.get_status("origin", branch)
|
|
86
|
+
if status.commits_behind > 0:
|
|
87
|
+
click.echo(f"Pulling {status.commits_behind} commits from remote...")
|
|
88
|
+
git.pull("origin", branch)
|
|
89
|
+
|
|
90
|
+
engine = SyncEngine(ctx["repo"], git, config_manager)
|
|
91
|
+
pulled = engine.rebuild_json_from_markdown()
|
|
92
|
+
if pulled:
|
|
93
|
+
click.echo(f"Imported {len(pulled)} fixes from team repository.")
|
|
94
|
+
|
|
95
|
+
except GitError:
|
|
96
|
+
click.echo("Remote repository appears to be empty. Ready for first push.")
|
|
97
|
+
|
|
98
|
+
click.echo("")
|
|
99
|
+
click.echo("Sync initialized successfully!")
|
|
100
|
+
click.echo(f" Repository: {repo_url}")
|
|
101
|
+
click.echo(f" Branch: {branch}")
|
|
102
|
+
click.echo(f" Author: {name} <{email}>")
|
|
103
|
+
click.echo("")
|
|
104
|
+
click.echo("Next steps:")
|
|
105
|
+
click.echo(" fixdoc sync push - Push your fixes to the team repo")
|
|
106
|
+
click.echo(" fixdoc sync pull - Pull fixes from teammates")
|
|
107
|
+
click.echo(" fixdoc sync status - Check sync status")
|
|
108
|
+
|
|
109
|
+
except GitError as e:
|
|
110
|
+
click.echo(f"Error: {e}", err=True)
|
|
111
|
+
raise SystemExit(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@sync.command()
|
|
115
|
+
@click.option("--message", "-m", default=None, help="Commit message")
|
|
116
|
+
@click.option("--all", "-a", "push_all", is_flag=True, help="Push all local fixes")
|
|
117
|
+
def push(message: Optional[str], push_all: bool):
|
|
118
|
+
"""
|
|
119
|
+
Push local fixes to the shared repository.
|
|
120
|
+
|
|
121
|
+
\b
|
|
122
|
+
Example:
|
|
123
|
+
fixdoc sync push
|
|
124
|
+
fixdoc sync push -m "Added storage account fix"
|
|
125
|
+
"""
|
|
126
|
+
ctx = get_sync_context()
|
|
127
|
+
engine = SyncEngine(ctx["repo"], ctx["git"], ctx["config_manager"])
|
|
128
|
+
|
|
129
|
+
if not ctx["config_manager"].is_sync_configured():
|
|
130
|
+
click.echo("Sync not configured. Run 'fixdoc sync init <repo-url>' first.", err=True)
|
|
131
|
+
raise SystemExit(1)
|
|
132
|
+
|
|
133
|
+
config = ctx["config_manager"].load()
|
|
134
|
+
if config.sync.auto_pull:
|
|
135
|
+
click.echo("Auto-pulling latest changes...")
|
|
136
|
+
pull_result = engine.execute_pull()
|
|
137
|
+
if not pull_result.success and pull_result.conflicts:
|
|
138
|
+
click.echo("Conflicts detected. Please resolve with 'fixdoc sync pull' first.", err=True)
|
|
139
|
+
raise SystemExit(1)
|
|
140
|
+
|
|
141
|
+
fixes = engine.prepare_push(push_all=push_all)
|
|
142
|
+
|
|
143
|
+
if not fixes:
|
|
144
|
+
click.echo("No new or modified fixes to push.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
click.echo(f"Pushing {len(fixes)} fix(es)...")
|
|
148
|
+
for fix in fixes:
|
|
149
|
+
click.echo(f" {fix.id[:8]} - {fix.issue[:40]}...")
|
|
150
|
+
|
|
151
|
+
result = engine.execute_push(fixes, commit_message=message)
|
|
152
|
+
|
|
153
|
+
if result.success:
|
|
154
|
+
if result.pushed_fixes:
|
|
155
|
+
click.echo(f"Successfully pushed {len(result.pushed_fixes)} fix(es).")
|
|
156
|
+
elif result.error_message:
|
|
157
|
+
click.echo(result.error_message)
|
|
158
|
+
else:
|
|
159
|
+
click.echo(f"Push failed: {result.error_message}", err=True)
|
|
160
|
+
if "rejected" in (result.error_message or "").lower():
|
|
161
|
+
click.echo("Hint: Run 'fixdoc sync pull' first to get the latest changes.")
|
|
162
|
+
raise SystemExit(1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@sync.command()
|
|
166
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite local changes on conflict")
|
|
167
|
+
def pull(force: bool):
|
|
168
|
+
"""
|
|
169
|
+
Pull fixes from the shared repository.
|
|
170
|
+
|
|
171
|
+
\b
|
|
172
|
+
Example:
|
|
173
|
+
fixdoc sync pull
|
|
174
|
+
fixdoc sync pull --force # Accept all remote changes
|
|
175
|
+
"""
|
|
176
|
+
ctx = get_sync_context()
|
|
177
|
+
engine = SyncEngine(ctx["repo"], ctx["git"], ctx["config_manager"])
|
|
178
|
+
|
|
179
|
+
if not ctx["config_manager"].is_sync_configured():
|
|
180
|
+
click.echo("Sync not configured. Run 'fixdoc sync init <repo-url>' first.", err=True)
|
|
181
|
+
raise SystemExit(1)
|
|
182
|
+
|
|
183
|
+
click.echo("Pulling from remote...")
|
|
184
|
+
result = engine.execute_pull(force=force)
|
|
185
|
+
|
|
186
|
+
if result.success:
|
|
187
|
+
if result.pulled_fixes:
|
|
188
|
+
click.echo(f"Successfully pulled/updated {len(result.pulled_fixes)} fix(es).")
|
|
189
|
+
for fix_id in result.pulled_fixes[:5]:
|
|
190
|
+
click.echo(f" {fix_id[:8]}")
|
|
191
|
+
if len(result.pulled_fixes) > 5:
|
|
192
|
+
click.echo(f" ... and {len(result.pulled_fixes) - 5} more")
|
|
193
|
+
else:
|
|
194
|
+
click.echo("Already up to date.")
|
|
195
|
+
elif result.conflicts:
|
|
196
|
+
click.echo(f"Conflicts detected in {len(result.conflicts)} fix(es):", err=True)
|
|
197
|
+
for conflict in result.conflicts:
|
|
198
|
+
click.echo(f" {conflict.fix_id[:8]} - {conflict.conflict_type.value}")
|
|
199
|
+
click.echo("")
|
|
200
|
+
click.echo("To resolve:")
|
|
201
|
+
click.echo(" fixdoc sync pull --force # Accept all remote changes")
|
|
202
|
+
click.echo(" # Or manually edit the conflicting fixes and push again")
|
|
203
|
+
raise SystemExit(1)
|
|
204
|
+
else:
|
|
205
|
+
click.echo(f"Pull failed: {result.error_message}", err=True)
|
|
206
|
+
raise SystemExit(1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@sync.command()
|
|
210
|
+
def status():
|
|
211
|
+
"""
|
|
212
|
+
Show sync status (ahead/behind commits, local changes).
|
|
213
|
+
|
|
214
|
+
\b
|
|
215
|
+
Example:
|
|
216
|
+
fixdoc sync status
|
|
217
|
+
"""
|
|
218
|
+
ctx = get_sync_context()
|
|
219
|
+
engine = SyncEngine(ctx["repo"], ctx["git"], ctx["config_manager"])
|
|
220
|
+
|
|
221
|
+
status_info = engine.get_sync_status()
|
|
222
|
+
|
|
223
|
+
if not status_info["configured"]:
|
|
224
|
+
click.echo("Sync not configured.")
|
|
225
|
+
click.echo("")
|
|
226
|
+
click.echo("To set up sync, run:")
|
|
227
|
+
click.echo(" fixdoc sync init <repo-url>")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
click.echo("Sync Status")
|
|
231
|
+
click.echo("=" * 40)
|
|
232
|
+
click.echo(f"Repository: {status_info['remote_url']}")
|
|
233
|
+
click.echo(f"Branch: {status_info['branch']}")
|
|
234
|
+
click.echo("")
|
|
235
|
+
|
|
236
|
+
status_val = status_info["status"]
|
|
237
|
+
if status_val == SyncStatus.UP_TO_DATE.value:
|
|
238
|
+
click.echo("Status: Up to date")
|
|
239
|
+
elif status_val == SyncStatus.AHEAD.value:
|
|
240
|
+
click.echo(f"Status: {status_info['commits_ahead']} commit(s) ahead")
|
|
241
|
+
elif status_val == SyncStatus.BEHIND.value:
|
|
242
|
+
click.echo(f"Status: {status_info['commits_behind']} commit(s) behind")
|
|
243
|
+
elif status_val == SyncStatus.DIVERGED.value:
|
|
244
|
+
click.echo(
|
|
245
|
+
f"Status: Diverged ({status_info['commits_ahead']} ahead, "
|
|
246
|
+
f"{status_info['commits_behind']} behind)"
|
|
247
|
+
)
|
|
248
|
+
elif status_val == SyncStatus.NO_REMOTE.value:
|
|
249
|
+
click.echo("Status: No remote configured")
|
|
250
|
+
|
|
251
|
+
click.echo("")
|
|
252
|
+
click.echo(f"Total fixes: {status_info['total_fixes']}")
|
|
253
|
+
click.echo(f"Pushable fixes: {status_info['pushable_fixes']}")
|
|
254
|
+
click.echo(f"Private fixes: {status_info['private_fixes']}")
|
|
255
|
+
|
|
256
|
+
if status_info["local_changes"]:
|
|
257
|
+
click.echo("")
|
|
258
|
+
click.echo("Local changes:")
|
|
259
|
+
for change in status_info["local_changes"][:5]:
|
|
260
|
+
click.echo(f" {change}")
|
|
261
|
+
if len(status_info["local_changes"]) > 5:
|
|
262
|
+
click.echo(f" ... and {len(status_info['local_changes']) - 5} more")
|
|
263
|
+
|
|
264
|
+
click.echo("")
|
|
265
|
+
if status_info["pushable_fixes"] > 0:
|
|
266
|
+
click.echo("Run 'fixdoc sync push' to share your fixes.")
|
|
267
|
+
if status_val == SyncStatus.BEHIND.value:
|
|
268
|
+
click.echo("Run 'fixdoc sync pull' to get team updates.")
|
fixdoc/config.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Configuration management for fixdoc sync."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SyncConfig:
|
|
12
|
+
"""Configuration for Git sync operations."""
|
|
13
|
+
|
|
14
|
+
remote_url: Optional[str] = None
|
|
15
|
+
branch: str = "main"
|
|
16
|
+
auto_pull: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class UserConfig:
|
|
21
|
+
"""User identity for attribution."""
|
|
22
|
+
|
|
23
|
+
name: Optional[str] = None
|
|
24
|
+
email: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class FixDocConfig:
|
|
29
|
+
"""Root configuration object."""
|
|
30
|
+
|
|
31
|
+
sync: SyncConfig = field(default_factory=SyncConfig)
|
|
32
|
+
user: UserConfig = field(default_factory=UserConfig)
|
|
33
|
+
private_fixes: list[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""Convert config to dictionary for YAML serialization."""
|
|
37
|
+
return {
|
|
38
|
+
"sync": asdict(self.sync),
|
|
39
|
+
"user": asdict(self.user),
|
|
40
|
+
"private_fixes": self.private_fixes,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: dict) -> "FixDocConfig":
|
|
45
|
+
"""Create config from dictionary loaded from YAML."""
|
|
46
|
+
sync_data = data.get("sync", {})
|
|
47
|
+
user_data = data.get("user", {})
|
|
48
|
+
private_fixes = data.get("private_fixes", [])
|
|
49
|
+
|
|
50
|
+
return cls(
|
|
51
|
+
sync=SyncConfig(
|
|
52
|
+
remote_url=sync_data.get("remote_url"),
|
|
53
|
+
branch=sync_data.get("branch", "main"),
|
|
54
|
+
auto_pull=sync_data.get("auto_pull", False),
|
|
55
|
+
),
|
|
56
|
+
user=UserConfig(
|
|
57
|
+
name=user_data.get("name"),
|
|
58
|
+
email=user_data.get("email"),
|
|
59
|
+
),
|
|
60
|
+
private_fixes=private_fixes,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConfigManager:
|
|
65
|
+
"""Manages ~/.fixdoc/config.yaml."""
|
|
66
|
+
|
|
67
|
+
CONFIG_FILE = "config.yaml"
|
|
68
|
+
|
|
69
|
+
def __init__(self, base_path: Optional[Path] = None):
|
|
70
|
+
self.base_path = base_path or Path.home() / ".fixdoc"
|
|
71
|
+
self.config_path = self.base_path / self.CONFIG_FILE
|
|
72
|
+
|
|
73
|
+
def load(self) -> FixDocConfig:
|
|
74
|
+
"""Load config from YAML, return defaults if not exists."""
|
|
75
|
+
if not self.config_path.exists():
|
|
76
|
+
return FixDocConfig()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with open(self.config_path, "r") as f:
|
|
80
|
+
data = yaml.safe_load(f) or {}
|
|
81
|
+
return FixDocConfig.from_dict(data)
|
|
82
|
+
except (yaml.YAMLError, IOError):
|
|
83
|
+
return FixDocConfig()
|
|
84
|
+
|
|
85
|
+
def save(self, config: FixDocConfig) -> None:
|
|
86
|
+
"""Save config to YAML."""
|
|
87
|
+
self.base_path.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
with open(self.config_path, "w") as f:
|
|
89
|
+
yaml.safe_dump(config.to_dict(), f, default_flow_style=False, sort_keys=False)
|
|
90
|
+
|
|
91
|
+
def is_sync_configured(self) -> bool:
|
|
92
|
+
"""Check if sync has been initialized."""
|
|
93
|
+
config = self.load()
|
|
94
|
+
return config.sync.remote_url is not None
|
|
95
|
+
|
|
96
|
+
def add_private_fix(self, fix_id: str) -> None:
|
|
97
|
+
"""Add a fix ID to the private list."""
|
|
98
|
+
config = self.load()
|
|
99
|
+
if fix_id not in config.private_fixes:
|
|
100
|
+
config.private_fixes.append(fix_id)
|
|
101
|
+
self.save(config)
|
|
102
|
+
|
|
103
|
+
def remove_private_fix(self, fix_id: str) -> None:
|
|
104
|
+
"""Remove a fix ID from the private list."""
|
|
105
|
+
config = self.load()
|
|
106
|
+
if fix_id in config.private_fixes:
|
|
107
|
+
config.private_fixes.remove(fix_id)
|
|
108
|
+
self.save(config)
|
|
109
|
+
|
|
110
|
+
def is_fix_private(self, fix_id: str) -> bool:
|
|
111
|
+
"""Check if a fix is marked as private."""
|
|
112
|
+
config = self.load()
|
|
113
|
+
return fix_id in config.private_fixes
|
fixdoc/fix.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FixDoc - Capture and search infrastructure fixes for cloud engineers.
|
|
4
|
+
|
|
5
|
+
This is the main entry point for the fixdoc CLI tool.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .cli import create_cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
cli = create_cli()
|
|
15
|
+
cli()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
sys.exit(main())
|
fixdoc/formatter.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Markdown formatting for fixes."""
|
|
2
|
+
|
|
3
|
+
from .models import Fix
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fix_to_markdown(fix: Fix) -> str:
|
|
7
|
+
"""Generate markdown documentation for a fix."""
|
|
8
|
+
lines = [f"# Fix: {fix.id[:8]}","",f"**Created:** {fix.created_at}","",f"**Updated:** {fix.updated_at}","",]
|
|
9
|
+
|
|
10
|
+
if fix.author:
|
|
11
|
+
lines.append(f"**Author:** {fix.author}")
|
|
12
|
+
if fix.author_email:
|
|
13
|
+
lines.append(f"**Author Email:** {fix.author_email}")
|
|
14
|
+
|
|
15
|
+
lines.append("")
|
|
16
|
+
|
|
17
|
+
if fix.author:
|
|
18
|
+
lines.append(f"**Author:** {fix.author}")
|
|
19
|
+
if fix.author_email:
|
|
20
|
+
lines.append(f"**Author Email:** {fix.author_email}")
|
|
21
|
+
|
|
22
|
+
lines.append("")
|
|
23
|
+
|
|
24
|
+
if fix.tags:
|
|
25
|
+
lines.extend([f"**Tags:** `{fix.tags}`", ""])
|
|
26
|
+
|
|
27
|
+
lines.extend(
|
|
28
|
+
[
|
|
29
|
+
"## Issue",
|
|
30
|
+
"",
|
|
31
|
+
fix.issue,
|
|
32
|
+
"",
|
|
33
|
+
"## Resolution",
|
|
34
|
+
"",
|
|
35
|
+
fix.resolution,
|
|
36
|
+
"",
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if fix.error_excerpt:
|
|
41
|
+
lines.extend(
|
|
42
|
+
[
|
|
43
|
+
"## Error Excerpt",
|
|
44
|
+
"",
|
|
45
|
+
"```",
|
|
46
|
+
fix.error_excerpt,
|
|
47
|
+
"```",
|
|
48
|
+
"",
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if fix.notes:
|
|
53
|
+
lines.extend(
|
|
54
|
+
[
|
|
55
|
+
"## Notes",
|
|
56
|
+
"",
|
|
57
|
+
fix.notes,
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return "\n".join(lines)
|