redgit 1.2.0__tar.gz → 1.2.2__tar.gz
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.
- {redgit-1.2.0/redgit.egg-info → redgit-1.2.2}/PKG-INFO +2 -2
- {redgit-1.2.0 → redgit-1.2.2}/README.md +1 -1
- {redgit-1.2.0 → redgit-1.2.2}/pyproject.toml +1 -1
- redgit-1.2.2/redgit/__init__.py +1 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/cli.py +11 -3
- redgit-1.2.2/redgit/commands/daily.py +342 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/integration.py +91 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/propose.py +537 -33
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/push.py +138 -14
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/config.py +81 -1
- redgit-1.2.2/redgit/core/daily_state.py +114 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/gitops.py +385 -10
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/prompt.py +1 -1
- {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/base.py +4 -2
- {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/registry.py +21 -11
- redgit-1.2.2/redgit/prompts/daily/default.md +36 -0
- {redgit-1.2.0 → redgit-1.2.2/redgit.egg-info}/PKG-INFO +2 -2
- {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/SOURCES.txt +3 -0
- redgit-1.2.0/redgit/__init__.py +0 -1
- {redgit-1.2.0 → redgit-1.2.2}/LICENSE +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/ci.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/config.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/init.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/notify.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/plugin.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/quality.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/scout.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/tap.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/llm.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/llm_providers.json +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/scout/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/scout/team.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/semgrep.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/core/tap.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/registry.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/base.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/commit/default.md +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/commit/minimal.md +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/quality/default.md +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/splash.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/__init__.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/console.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/editor.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/security.py +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/dependency_links.txt +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/entry_points.txt +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/requires.txt +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/top_level.txt +0 -0
- {redgit-1.2.0 → redgit-1.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redgit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: AI-powered Git workflow assistant with task management integration
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,7 +34,7 @@ Requires-Dist: ruff>=0.4.0
|
|
|
34
34
|
Dynamic: license-file
|
|
35
35
|
|
|
36
36
|
<p align="center">
|
|
37
|
-
<img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.
|
|
37
|
+
<img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.2" alt="RedGit Logo" width="400"/>
|
|
38
38
|
</p>
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.2" alt="RedGit Logo" width="400"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.2"
|
|
@@ -8,6 +8,7 @@ from redgit.splash import splash
|
|
|
8
8
|
from redgit.commands.init import init_cmd
|
|
9
9
|
from redgit.commands.propose import propose_cmd
|
|
10
10
|
from redgit.commands.push import push_cmd
|
|
11
|
+
from redgit.commands.daily import daily_cmd
|
|
11
12
|
from redgit.commands.integration import integration_app
|
|
12
13
|
from redgit.commands.plugin import plugin_app
|
|
13
14
|
from redgit.commands.tap import tap_app, install_cmd as tap_install_cmd, uninstall_cmd as tap_uninstall_cmd
|
|
@@ -48,6 +49,7 @@ def main_callback(
|
|
|
48
49
|
app.command("init")(init_cmd)
|
|
49
50
|
app.command("propose")(propose_cmd)
|
|
50
51
|
app.command("push")(push_cmd)
|
|
52
|
+
app.command("daily")(daily_cmd)
|
|
51
53
|
app.command("install")(tap_install_cmd)
|
|
52
54
|
app.command("uninstall")(tap_uninstall_cmd)
|
|
53
55
|
app.add_typer(integration_app, name="integration")
|
|
@@ -73,10 +75,16 @@ def _load_plugin_commands():
|
|
|
73
75
|
for name, cmd_app in commands.items():
|
|
74
76
|
app.add_typer(cmd_app, name=name)
|
|
75
77
|
|
|
76
|
-
# Load plugin shortcuts (e.g., release_shortcut -> rg release)
|
|
78
|
+
# Load plugin shortcuts (e.g., release_shortcut -> rg release, release_app -> rg release)
|
|
79
|
+
import typer
|
|
77
80
|
shortcuts = get_all_plugin_shortcuts(config)
|
|
78
|
-
for name,
|
|
79
|
-
|
|
81
|
+
for name, cmd in shortcuts.items():
|
|
82
|
+
if isinstance(cmd, typer.Typer):
|
|
83
|
+
# Typer app shortcut (e.g., release_app -> rg release with subcommands)
|
|
84
|
+
app.add_typer(cmd, name=name)
|
|
85
|
+
else:
|
|
86
|
+
# Function shortcut (e.g., release_shortcut -> rg release)
|
|
87
|
+
app.command(name)(cmd)
|
|
80
88
|
|
|
81
89
|
except Exception:
|
|
82
90
|
# Silently fail if config not found (e.g., before init)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daily command - Generate daily activity report from git history.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Optional, List, Dict
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from ..core.config import ConfigManager, RETGIT_DIR
|
|
16
|
+
from ..core.daily_state import DailyStateManager
|
|
17
|
+
from ..core.gitops import GitOps, NotAGitRepoError
|
|
18
|
+
from ..core.llm import LLMClient
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# Language display names
|
|
23
|
+
LANGUAGE_NAMES = {
|
|
24
|
+
"en": "English",
|
|
25
|
+
"tr": "Turkish",
|
|
26
|
+
"de": "German",
|
|
27
|
+
"fr": "French",
|
|
28
|
+
"es": "Spanish",
|
|
29
|
+
"it": "Italian",
|
|
30
|
+
"pt": "Portuguese",
|
|
31
|
+
"nl": "Dutch",
|
|
32
|
+
"ru": "Russian",
|
|
33
|
+
"zh": "Chinese",
|
|
34
|
+
"ja": "Japanese",
|
|
35
|
+
"ko": "Korean",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_commits_since(since: datetime, author: Optional[str] = None) -> List[Dict]:
|
|
40
|
+
"""
|
|
41
|
+
Get commits since a given timestamp.
|
|
42
|
+
|
|
43
|
+
Returns list of dicts with commit info:
|
|
44
|
+
- hash: commit hash
|
|
45
|
+
- author: author name
|
|
46
|
+
- email: author email
|
|
47
|
+
- timestamp: unix timestamp
|
|
48
|
+
- date: formatted date
|
|
49
|
+
- message: commit message
|
|
50
|
+
- files: list of changed files with stats
|
|
51
|
+
"""
|
|
52
|
+
since_str = since.strftime("%Y-%m-%dT%H:%M:%S")
|
|
53
|
+
|
|
54
|
+
# Build git log command
|
|
55
|
+
cmd = [
|
|
56
|
+
"git", "log",
|
|
57
|
+
f"--since={since_str}",
|
|
58
|
+
"--format=%H|%an|%ae|%at|%s",
|
|
59
|
+
"--numstat"
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
if author:
|
|
63
|
+
cmd.append(f"--author={author}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
67
|
+
except subprocess.CalledProcessError as e:
|
|
68
|
+
console.print(f"[red]Git error: {e.stderr}[/red]")
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
output = result.stdout.strip()
|
|
72
|
+
if not output:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
commits = []
|
|
76
|
+
current_commit = None
|
|
77
|
+
|
|
78
|
+
for line in output.split('\n'):
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Check if it's a commit line (has | separators)
|
|
83
|
+
if '|' in line and line.count('|') >= 4:
|
|
84
|
+
# Save previous commit
|
|
85
|
+
if current_commit:
|
|
86
|
+
commits.append(current_commit)
|
|
87
|
+
|
|
88
|
+
parts = line.split('|', 4)
|
|
89
|
+
if len(parts) >= 5:
|
|
90
|
+
timestamp = int(parts[3])
|
|
91
|
+
# Clean author name (remove literal \n if present)
|
|
92
|
+
author = parts[1].replace('\\n', '').strip()
|
|
93
|
+
current_commit = {
|
|
94
|
+
"hash": parts[0][:8], # Short hash
|
|
95
|
+
"author": author,
|
|
96
|
+
"email": parts[2],
|
|
97
|
+
"timestamp": timestamp,
|
|
98
|
+
"date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M"),
|
|
99
|
+
"message": parts[4],
|
|
100
|
+
"files": [],
|
|
101
|
+
"additions": 0,
|
|
102
|
+
"deletions": 0,
|
|
103
|
+
}
|
|
104
|
+
elif current_commit and '\t' in line:
|
|
105
|
+
# File stat line: additions\tdeletions\tfilename
|
|
106
|
+
parts = line.split('\t')
|
|
107
|
+
if len(parts) >= 3:
|
|
108
|
+
additions = int(parts[0]) if parts[0].isdigit() else 0
|
|
109
|
+
deletions = int(parts[1]) if parts[1].isdigit() else 0
|
|
110
|
+
filename = parts[2]
|
|
111
|
+
current_commit["files"].append({
|
|
112
|
+
"name": filename,
|
|
113
|
+
"additions": additions,
|
|
114
|
+
"deletions": deletions,
|
|
115
|
+
})
|
|
116
|
+
current_commit["additions"] += additions
|
|
117
|
+
current_commit["deletions"] += deletions
|
|
118
|
+
|
|
119
|
+
# Don't forget the last commit
|
|
120
|
+
if current_commit:
|
|
121
|
+
commits.append(current_commit)
|
|
122
|
+
|
|
123
|
+
return commits
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def format_commits_for_llm(commits: List[Dict]) -> str:
|
|
127
|
+
"""Format commits for LLM prompt."""
|
|
128
|
+
lines = []
|
|
129
|
+
for c in commits:
|
|
130
|
+
lines.append(f"- [{c['hash']}] {c['message']} (by {c['author']}, {c['date']})")
|
|
131
|
+
if c["files"]:
|
|
132
|
+
for f in c["files"][:5]: # Limit files per commit
|
|
133
|
+
lines.append(f" {f['name']} (+{f['additions']}/-{f['deletions']})")
|
|
134
|
+
if len(c["files"]) > 5:
|
|
135
|
+
lines.append(f" ... and {len(c['files']) - 5} more files")
|
|
136
|
+
return '\n'.join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def calculate_stats(commits: List[Dict]) -> Dict:
|
|
140
|
+
"""Calculate aggregate statistics from commits."""
|
|
141
|
+
stats = {
|
|
142
|
+
"total_commits": len(commits),
|
|
143
|
+
"total_additions": 0,
|
|
144
|
+
"total_deletions": 0,
|
|
145
|
+
"total_files": 0,
|
|
146
|
+
"authors": set(),
|
|
147
|
+
"directories": defaultdict(int),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
seen_files = set()
|
|
151
|
+
|
|
152
|
+
for c in commits:
|
|
153
|
+
stats["total_additions"] += c["additions"]
|
|
154
|
+
stats["total_deletions"] += c["deletions"]
|
|
155
|
+
stats["authors"].add(c["author"].strip())
|
|
156
|
+
|
|
157
|
+
for f in c["files"]:
|
|
158
|
+
if f["name"] not in seen_files:
|
|
159
|
+
seen_files.add(f["name"])
|
|
160
|
+
stats["total_files"] += 1
|
|
161
|
+
|
|
162
|
+
# Track directories
|
|
163
|
+
if '/' in f["name"]:
|
|
164
|
+
dir_name = f["name"].split('/')[0]
|
|
165
|
+
stats["directories"][dir_name] += 1
|
|
166
|
+
|
|
167
|
+
stats["authors"] = list(stats["authors"])
|
|
168
|
+
stats["directories"] = dict(sorted(
|
|
169
|
+
stats["directories"].items(),
|
|
170
|
+
key=lambda x: x[1],
|
|
171
|
+
reverse=True
|
|
172
|
+
)[:10]) # Top 10 directories
|
|
173
|
+
|
|
174
|
+
return stats
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_daily_prompt(language: str) -> str:
|
|
178
|
+
"""Load the daily prompt template."""
|
|
179
|
+
# Check project-specific first
|
|
180
|
+
project_prompt = RETGIT_DIR / "prompts" / "daily" / "default.md"
|
|
181
|
+
if project_prompt.exists():
|
|
182
|
+
return project_prompt.read_text()
|
|
183
|
+
|
|
184
|
+
# Fallback to builtin
|
|
185
|
+
builtin_prompt = Path(__file__).parent.parent / "prompts" / "daily" / "default.md"
|
|
186
|
+
if builtin_prompt.exists():
|
|
187
|
+
return builtin_prompt.read_text()
|
|
188
|
+
|
|
189
|
+
# Hardcoded fallback
|
|
190
|
+
return f"""Analyze these git commits and create a daily report in {language}:
|
|
191
|
+
|
|
192
|
+
{{{{COMMITS}}}}
|
|
193
|
+
|
|
194
|
+
Provide:
|
|
195
|
+
1. Summary (2-3 sentences)
|
|
196
|
+
2. Key changes (bullet points)
|
|
197
|
+
3. Affected areas
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def daily_cmd(
|
|
202
|
+
since: Optional[str] = typer.Option(
|
|
203
|
+
None, "--since", "-s",
|
|
204
|
+
help="Override start time (e.g., '24h', '2d', 'yesterday', '2024-01-15')"
|
|
205
|
+
),
|
|
206
|
+
author: Optional[str] = typer.Option(
|
|
207
|
+
None, "--author", "-a",
|
|
208
|
+
help="Filter commits by author name"
|
|
209
|
+
),
|
|
210
|
+
verbose: bool = typer.Option(
|
|
211
|
+
False, "--verbose", "-v",
|
|
212
|
+
help="Show detailed output including raw commit data"
|
|
213
|
+
),
|
|
214
|
+
no_ai: bool = typer.Option(
|
|
215
|
+
False, "--no-ai",
|
|
216
|
+
help="Skip AI analysis, show only raw stats"
|
|
217
|
+
),
|
|
218
|
+
):
|
|
219
|
+
"""Generate a daily report of git activity since last run."""
|
|
220
|
+
|
|
221
|
+
# Check git repo
|
|
222
|
+
try:
|
|
223
|
+
GitOps()
|
|
224
|
+
except NotAGitRepoError:
|
|
225
|
+
console.print("[red]Not a git repository.[/red]")
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
config_manager = ConfigManager()
|
|
229
|
+
config = config_manager.load()
|
|
230
|
+
state_manager = DailyStateManager()
|
|
231
|
+
|
|
232
|
+
# Get language from config
|
|
233
|
+
daily_config = config.get("daily", {})
|
|
234
|
+
language = daily_config.get("language", "en")
|
|
235
|
+
language_name = LANGUAGE_NAMES.get(language, language)
|
|
236
|
+
|
|
237
|
+
# Determine since timestamp
|
|
238
|
+
# If author filter is used without --since, default to 24h (don't use last_run)
|
|
239
|
+
if since:
|
|
240
|
+
since_dt = state_manager.parse_since_option(since)
|
|
241
|
+
time_desc = f"since {since}"
|
|
242
|
+
elif author:
|
|
243
|
+
# When filtering by author, default to 24h instead of last_run
|
|
244
|
+
from datetime import timedelta
|
|
245
|
+
since_dt = datetime.now() - timedelta(hours=24)
|
|
246
|
+
time_desc = "last 24 hours"
|
|
247
|
+
else:
|
|
248
|
+
since_dt = state_manager.get_since_timestamp()
|
|
249
|
+
last_run = state_manager.get_last_run()
|
|
250
|
+
if last_run:
|
|
251
|
+
time_desc = f"since last run ({last_run.strftime('%Y-%m-%d %H:%M')})"
|
|
252
|
+
else:
|
|
253
|
+
time_desc = "last 24 hours (first run)"
|
|
254
|
+
|
|
255
|
+
if verbose:
|
|
256
|
+
console.print(f"[dim]Language: {language_name}[/dim]")
|
|
257
|
+
console.print(f"[dim]Since: {since_dt.isoformat()}[/dim]")
|
|
258
|
+
|
|
259
|
+
# Get commits
|
|
260
|
+
with console.status("Fetching commits..."):
|
|
261
|
+
commits = get_commits_since(since_dt, author)
|
|
262
|
+
|
|
263
|
+
if not commits:
|
|
264
|
+
console.print(Panel(
|
|
265
|
+
f"[yellow]No commits found {time_desc}[/yellow]",
|
|
266
|
+
title="Daily Report",
|
|
267
|
+
border_style="yellow"
|
|
268
|
+
))
|
|
269
|
+
# Only update last run if not filtering by author
|
|
270
|
+
if not author:
|
|
271
|
+
state_manager.set_last_run()
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Calculate stats
|
|
275
|
+
stats = calculate_stats(commits)
|
|
276
|
+
|
|
277
|
+
# Show header
|
|
278
|
+
today = datetime.now().strftime("%d %B %Y")
|
|
279
|
+
header_text = f"[bold]Daily Report - {today}[/bold]\n"
|
|
280
|
+
header_text += f"[dim]{time_desc} | {stats['total_commits']} commits | {len(stats['authors'])} author(s)[/dim]"
|
|
281
|
+
|
|
282
|
+
console.print(Panel(header_text, border_style="cyan"))
|
|
283
|
+
|
|
284
|
+
# Show verbose commit list
|
|
285
|
+
if verbose:
|
|
286
|
+
console.print("\n[bold cyan]Commits:[/bold cyan]")
|
|
287
|
+
for c in commits[:20]: # Limit display
|
|
288
|
+
console.print(f" [dim]{c['hash']}[/dim] {c['message']} [dim]({c['author']})[/dim]")
|
|
289
|
+
if len(commits) > 20:
|
|
290
|
+
console.print(f" [dim]... and {len(commits) - 20} more[/dim]")
|
|
291
|
+
console.print()
|
|
292
|
+
|
|
293
|
+
# Show stats table
|
|
294
|
+
stats_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
295
|
+
stats_table.add_column("Metric", style="cyan")
|
|
296
|
+
stats_table.add_column("Value", style="green")
|
|
297
|
+
|
|
298
|
+
stats_table.add_row("Commits", str(stats["total_commits"]))
|
|
299
|
+
stats_table.add_row("Lines added", f"+{stats['total_additions']}")
|
|
300
|
+
stats_table.add_row("Lines deleted", f"-{stats['total_deletions']}")
|
|
301
|
+
stats_table.add_row("Files changed", str(stats["total_files"]))
|
|
302
|
+
stats_table.add_row("Authors", ", ".join(stats["authors"]))
|
|
303
|
+
|
|
304
|
+
console.print(stats_table)
|
|
305
|
+
|
|
306
|
+
# Show affected directories
|
|
307
|
+
if stats["directories"]:
|
|
308
|
+
console.print("\n[bold]Affected areas:[/bold]")
|
|
309
|
+
for dir_name, count in list(stats["directories"].items())[:5]:
|
|
310
|
+
console.print(f" [dim]├──[/dim] {dir_name}/ [dim]({count} files)[/dim]")
|
|
311
|
+
|
|
312
|
+
# AI Analysis
|
|
313
|
+
if not no_ai:
|
|
314
|
+
console.print()
|
|
315
|
+
with console.status("Generating AI summary..."):
|
|
316
|
+
try:
|
|
317
|
+
llm_config = config.get("llm", {})
|
|
318
|
+
llm = LLMClient(llm_config)
|
|
319
|
+
|
|
320
|
+
# Load and prepare prompt
|
|
321
|
+
prompt_template = load_daily_prompt(language)
|
|
322
|
+
commits_text = format_commits_for_llm(commits)
|
|
323
|
+
prompt = prompt_template.replace("{{COMMITS}}", commits_text)
|
|
324
|
+
prompt = prompt.replace("{{LANGUAGE}}", language_name)
|
|
325
|
+
|
|
326
|
+
# Get AI response
|
|
327
|
+
report = llm.chat(prompt)
|
|
328
|
+
|
|
329
|
+
console.print(Panel(
|
|
330
|
+
report,
|
|
331
|
+
title="[bold]AI Summary[/bold]",
|
|
332
|
+
border_style="green"
|
|
333
|
+
))
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
console.print(f"[yellow]AI analysis skipped: {e}[/yellow]")
|
|
337
|
+
|
|
338
|
+
# Update last run timestamp (only if not filtering by author)
|
|
339
|
+
if not author:
|
|
340
|
+
state_manager.set_last_run()
|
|
341
|
+
if verbose:
|
|
342
|
+
console.print(f"\n[dim]Last run updated: {datetime.now().isoformat()}[/dim]")
|
|
@@ -430,6 +430,97 @@ def add_cmd(name: str):
|
|
|
430
430
|
typer.echo(f" ⚠️ Run 'redgit integration install {name}' to configure")
|
|
431
431
|
|
|
432
432
|
|
|
433
|
+
@integration_app.command("update")
|
|
434
|
+
def update_cmd(
|
|
435
|
+
name: str = typer.Argument(None, help="Integration name to update (or omit to update all)"),
|
|
436
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force reinstall even if up to date")
|
|
437
|
+
):
|
|
438
|
+
"""Update installed integrations from taps"""
|
|
439
|
+
from ..core.tap import TapManager, find_item_in_taps
|
|
440
|
+
from .tap import install_from_tap
|
|
441
|
+
|
|
442
|
+
tap_mgr = TapManager()
|
|
443
|
+
config = ConfigManager().load()
|
|
444
|
+
integrations_config = config.get("integrations", {})
|
|
445
|
+
|
|
446
|
+
# Get installed integrations
|
|
447
|
+
installed_names = _get_installed_integrations()
|
|
448
|
+
|
|
449
|
+
# Also include integrations enabled in config
|
|
450
|
+
for int_name, cfg in integrations_config.items():
|
|
451
|
+
if isinstance(cfg, dict) and cfg.get("enabled"):
|
|
452
|
+
installed_names.add(int_name)
|
|
453
|
+
|
|
454
|
+
if not installed_names:
|
|
455
|
+
typer.echo("\n📦 No integrations installed.\n")
|
|
456
|
+
typer.echo(" 💡 Install from taps: rg install <name>")
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
# Determine which integrations to update
|
|
460
|
+
if name:
|
|
461
|
+
# Update specific integration
|
|
462
|
+
# Normalize name
|
|
463
|
+
normalized_name = name.replace("-", "_")
|
|
464
|
+
if name not in installed_names and normalized_name not in installed_names:
|
|
465
|
+
typer.secho(f"❌ '{name}' is not installed.", fg=typer.colors.RED)
|
|
466
|
+
typer.echo(f" Installed: {', '.join(sorted(installed_names))}")
|
|
467
|
+
raise typer.Exit(1)
|
|
468
|
+
|
|
469
|
+
target_name = name if name in installed_names else normalized_name
|
|
470
|
+
to_update = [target_name]
|
|
471
|
+
else:
|
|
472
|
+
# Update all integrations
|
|
473
|
+
to_update = sorted(installed_names)
|
|
474
|
+
|
|
475
|
+
typer.echo(f"\n🔄 Updating {len(to_update)} integration(s)...\n")
|
|
476
|
+
|
|
477
|
+
updated = 0
|
|
478
|
+
failed = 0
|
|
479
|
+
skipped = 0
|
|
480
|
+
|
|
481
|
+
for int_name in to_update:
|
|
482
|
+
typer.echo(f" {int_name}...", nl=False)
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
# Find integration in taps
|
|
486
|
+
result = find_item_in_taps(int_name, "integration")
|
|
487
|
+
|
|
488
|
+
if not result:
|
|
489
|
+
# Try with hyphen variant
|
|
490
|
+
result = find_item_in_taps(int_name.replace("_", "-"), "integration")
|
|
491
|
+
|
|
492
|
+
if not result:
|
|
493
|
+
# Not from a tap, might be local custom integration
|
|
494
|
+
typer.secho(" skipped (local/custom)", fg=typer.colors.YELLOW)
|
|
495
|
+
skipped += 1
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
# Update from tap using install_from_tap with force=True
|
|
499
|
+
success = install_from_tap(int_name, force=True, no_configure=True)
|
|
500
|
+
|
|
501
|
+
if success:
|
|
502
|
+
typer.secho(" ✓ updated", fg=typer.colors.GREEN)
|
|
503
|
+
updated += 1
|
|
504
|
+
else:
|
|
505
|
+
typer.secho(" ✗ failed", fg=typer.colors.RED)
|
|
506
|
+
failed += 1
|
|
507
|
+
|
|
508
|
+
except Exception as e:
|
|
509
|
+
typer.secho(f" ✗ failed: {e}", fg=typer.colors.RED)
|
|
510
|
+
failed += 1
|
|
511
|
+
|
|
512
|
+
# Summary
|
|
513
|
+
typer.echo("")
|
|
514
|
+
if updated > 0:
|
|
515
|
+
typer.secho(f"✅ Updated {updated} integration(s)", fg=typer.colors.GREEN)
|
|
516
|
+
if skipped > 0:
|
|
517
|
+
typer.echo(f" Skipped: {skipped}")
|
|
518
|
+
if failed > 0:
|
|
519
|
+
typer.secho(f" Failed: {failed}", fg=typer.colors.RED)
|
|
520
|
+
|
|
521
|
+
typer.echo("")
|
|
522
|
+
|
|
523
|
+
|
|
433
524
|
@integration_app.command("remove")
|
|
434
525
|
def remove_cmd(name: str):
|
|
435
526
|
"""Disable an integration"""
|