gitview 0.1.1__tar.gz → 0.1.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.
- {gitview-0.1.1/gitview.egg-info → gitview-0.1.2}/PKG-INFO +1 -1
- {gitview-0.1.1 → gitview-0.1.2}/gitview/__init__.py +1 -1
- {gitview-0.1.1 → gitview-0.1.2}/gitview/cli.py +157 -15
- {gitview-0.1.1 → gitview-0.1.2}/gitview/extractor.py +75 -7
- {gitview-0.1.1 → gitview-0.1.2}/gitview/writer.py +43 -3
- {gitview-0.1.1 → gitview-0.1.2/gitview.egg-info}/PKG-INFO +1 -1
- {gitview-0.1.1 → gitview-0.1.2}/pyproject.toml +1 -1
- {gitview-0.1.1 → gitview-0.1.2}/INSTALL.md +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/LICENSE +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/MANIFEST.in +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/README.md +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/bin/gitview +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/examples/basic_usage.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/__init__.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/anthropic_backend.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/base.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/ollama_backend.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/openai_backend.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/router.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/chunker.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/storyteller.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview/summarizer.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/SOURCES.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/dependency_links.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/entry_points.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/not-zip-safe +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/requires.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/top_level.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/requirements.txt +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/setup.cfg +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/setup.py +0 -0
- {gitview-0.1.1 → gitview-0.1.2}/verify_installation.py +0 -0
|
@@ -125,6 +125,36 @@ EXAMPLES:
|
|
|
125
125
|
|
|
126
126
|
# Use custom Ollama server
|
|
127
127
|
gitview analyze --backend ollama --ollama-url http://192.168.1.100:11434
|
|
128
|
+
|
|
129
|
+
\b
|
|
130
|
+
INCREMENTAL ANALYSIS (Cost-Efficient Ongoing Monitoring):
|
|
131
|
+
|
|
132
|
+
For managers analyzing multiple projects on an ongoing basis, incremental
|
|
133
|
+
analysis dramatically reduces costs by reusing previous LLM summaries.
|
|
134
|
+
|
|
135
|
+
# Initial full analysis
|
|
136
|
+
gitview analyze --output reports/myproject
|
|
137
|
+
|
|
138
|
+
# Later: incremental update (only analyzes new commits)
|
|
139
|
+
gitview analyze --output reports/myproject --incremental
|
|
140
|
+
|
|
141
|
+
# Manual incremental from specific commit
|
|
142
|
+
gitview analyze --since-commit abc123def
|
|
143
|
+
|
|
144
|
+
# Incremental from date
|
|
145
|
+
gitview analyze --since-date 2025-11-01
|
|
146
|
+
|
|
147
|
+
How it works:
|
|
148
|
+
- Detects previous analysis in output directory
|
|
149
|
+
- Extracts only commits since last run
|
|
150
|
+
- Reuses existing phase summaries (no LLM calls!)
|
|
151
|
+
- Only summarizes new/modified phases
|
|
152
|
+
- Updates JSON with new metadata
|
|
153
|
+
|
|
154
|
+
Benefits:
|
|
155
|
+
- Massive API cost savings for ongoing monitoring
|
|
156
|
+
- Much faster analysis (only processes new commits)
|
|
157
|
+
- Perfect for CI/CD integration or periodic reviews
|
|
128
158
|
"""
|
|
129
159
|
|
|
130
160
|
|
|
@@ -159,8 +189,16 @@ EXAMPLES:
|
|
|
159
189
|
@click.option('--skip-llm', is_flag=True,
|
|
160
190
|
help="Skip LLM summarization - only extract and chunk history "
|
|
161
191
|
"(useful for quick analysis without API costs)")
|
|
192
|
+
@click.option('--incremental', is_flag=True,
|
|
193
|
+
help="Incremental analysis: only process commits since last run. "
|
|
194
|
+
"Automatically detects previous analysis in output directory")
|
|
195
|
+
@click.option('--since-commit',
|
|
196
|
+
help="Extract commits since this commit hash (for manual incremental analysis)")
|
|
197
|
+
@click.option('--since-date',
|
|
198
|
+
help="Extract commits since this date (ISO format: YYYY-MM-DD)")
|
|
162
199
|
def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
163
|
-
model, api_key, ollama_url, repo_name, skip_llm
|
|
200
|
+
model, api_key, ollama_url, repo_name, skip_llm, incremental,
|
|
201
|
+
since_commit, since_date):
|
|
164
202
|
"""Analyze git repository and generate narrative history.
|
|
165
203
|
|
|
166
204
|
This is the main command that runs the full pipeline:
|
|
@@ -196,6 +234,36 @@ def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
|
196
234
|
console.print("[yellow]Skipping LLM summarization[/yellow]\n")
|
|
197
235
|
|
|
198
236
|
try:
|
|
237
|
+
# Check for incremental analysis
|
|
238
|
+
previous_analysis = None
|
|
239
|
+
existing_phases = []
|
|
240
|
+
starting_loc = 0
|
|
241
|
+
|
|
242
|
+
if incremental or since_commit or since_date:
|
|
243
|
+
# Load previous analysis
|
|
244
|
+
previous_analysis = OutputWriter.load_previous_analysis(output)
|
|
245
|
+
|
|
246
|
+
if incremental and not previous_analysis:
|
|
247
|
+
console.print("[yellow]Warning: --incremental specified but no previous analysis found.[/yellow]")
|
|
248
|
+
console.print("[yellow]Running full analysis instead...[/yellow]\n")
|
|
249
|
+
incremental = False
|
|
250
|
+
elif previous_analysis:
|
|
251
|
+
metadata = previous_analysis.get('metadata', {})
|
|
252
|
+
last_hash = metadata.get('last_commit_hash')
|
|
253
|
+
last_date = metadata.get('last_commit_date')
|
|
254
|
+
|
|
255
|
+
if incremental:
|
|
256
|
+
since_commit = last_hash
|
|
257
|
+
console.print(f"[cyan]Incremental mode:[/cyan] Analyzing commits since {last_hash[:8]}")
|
|
258
|
+
console.print(f"[cyan]Last analysis:[/cyan] {metadata.get('generated_at', 'unknown')}\n")
|
|
259
|
+
|
|
260
|
+
# Load existing phases
|
|
261
|
+
from .chunker import Phase
|
|
262
|
+
existing_phases = [Phase.from_dict(p) for p in previous_analysis.get('phases', [])]
|
|
263
|
+
|
|
264
|
+
if existing_phases and existing_phases[-1].commits:
|
|
265
|
+
starting_loc = existing_phases[-1].commits[-1].loc_total
|
|
266
|
+
|
|
199
267
|
# Step 1: Extract git history
|
|
200
268
|
console.print("[bold]Step 1: Extracting git history...[/bold]")
|
|
201
269
|
extractor = GitHistoryExtractor(str(repo_path))
|
|
@@ -206,10 +274,32 @@ def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
|
206
274
|
console=console
|
|
207
275
|
) as progress:
|
|
208
276
|
task = progress.add_task("Extracting commits...", total=None)
|
|
209
|
-
|
|
277
|
+
|
|
278
|
+
# Use incremental extraction if requested
|
|
279
|
+
if since_commit or since_date:
|
|
280
|
+
records = extractor.extract_incremental(
|
|
281
|
+
since_commit=since_commit,
|
|
282
|
+
since_date=since_date,
|
|
283
|
+
branch=branch
|
|
284
|
+
)
|
|
285
|
+
# Adjust LOC to continue from previous analysis
|
|
286
|
+
if starting_loc > 0:
|
|
287
|
+
extractor._calculate_cumulative_loc(records, starting_loc)
|
|
288
|
+
else:
|
|
289
|
+
records = extractor.extract_history(max_commits=max_commits, branch=branch)
|
|
290
|
+
|
|
210
291
|
progress.update(task, completed=True)
|
|
211
292
|
|
|
212
|
-
|
|
293
|
+
if since_commit or since_date:
|
|
294
|
+
console.print(f"[green]Extracted {len(records)} new commits[/green]\n")
|
|
295
|
+
|
|
296
|
+
# Exit early if no new commits
|
|
297
|
+
if len(records) == 0:
|
|
298
|
+
console.print("[yellow]No new commits found since last analysis.[/yellow]")
|
|
299
|
+
console.print("[green]Repository is up to date![/green]\n")
|
|
300
|
+
return
|
|
301
|
+
else:
|
|
302
|
+
console.print(f"[green]Extracted {len(records)} commits[/green]\n")
|
|
213
303
|
|
|
214
304
|
# Save raw history
|
|
215
305
|
history_file = Path(output) / "repo_history.jsonl"
|
|
@@ -223,8 +313,47 @@ def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
|
223
313
|
if strategy == 'fixed':
|
|
224
314
|
kwargs['chunk_size'] = chunk_size
|
|
225
315
|
|
|
226
|
-
|
|
227
|
-
|
|
316
|
+
# Handle incremental phase management
|
|
317
|
+
if existing_phases and len(records) > 0:
|
|
318
|
+
# Incremental mode: merge new commits with existing phases
|
|
319
|
+
merge_threshold = 10 # commits - merge if fewer, create new phase if more
|
|
320
|
+
|
|
321
|
+
if len(records) < merge_threshold:
|
|
322
|
+
# Append new commits to last phase
|
|
323
|
+
console.print(f"[yellow]Merging {len(records)} new commits into last phase...[/yellow]")
|
|
324
|
+
last_phase = existing_phases[-1]
|
|
325
|
+
last_phase.commits.extend(records)
|
|
326
|
+
|
|
327
|
+
# Recalculate phase stats
|
|
328
|
+
from .chunker import Phase
|
|
329
|
+
last_phase.commit_count = len(last_phase.commits)
|
|
330
|
+
last_phase.end_date = records[-1].timestamp
|
|
331
|
+
last_phase.total_insertions = sum(c.insertions for c in last_phase.commits)
|
|
332
|
+
last_phase.total_deletions = sum(c.deletions for c in last_phase.commits)
|
|
333
|
+
last_phase.loc_end = records[-1].loc_total
|
|
334
|
+
last_phase.loc_delta = last_phase.loc_end - last_phase.loc_start
|
|
335
|
+
if last_phase.loc_start > 0:
|
|
336
|
+
last_phase.loc_delta_percent = (last_phase.loc_delta / last_phase.loc_start) * 100
|
|
337
|
+
|
|
338
|
+
# Clear summary so it will be regenerated
|
|
339
|
+
last_phase.summary = None
|
|
340
|
+
|
|
341
|
+
phases = existing_phases
|
|
342
|
+
console.print(f"[green]Updated last phase (now {last_phase.commit_count} commits)[/green]\n")
|
|
343
|
+
else:
|
|
344
|
+
# Create new phases for new commits
|
|
345
|
+
new_phases = chunker.chunk(records, **kwargs)
|
|
346
|
+
|
|
347
|
+
# Renumber new phases to continue from existing
|
|
348
|
+
for phase in new_phases:
|
|
349
|
+
phase.phase_number = len(existing_phases) + phase.phase_number
|
|
350
|
+
|
|
351
|
+
phases = existing_phases + new_phases
|
|
352
|
+
console.print(f"[green]Created {len(new_phases)} new phases (total: {len(phases)})[/green]\n")
|
|
353
|
+
else:
|
|
354
|
+
# Full analysis: chunk normally
|
|
355
|
+
phases = chunker.chunk(records, **kwargs)
|
|
356
|
+
console.print(f"[green]Created {len(phases)} phases[/green]\n")
|
|
228
357
|
|
|
229
358
|
# Display phase overview
|
|
230
359
|
_display_phase_overview(phases)
|
|
@@ -249,31 +378,44 @@ def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
|
249
378
|
ollama_url=ollama_url
|
|
250
379
|
)
|
|
251
380
|
|
|
381
|
+
# Identify phases that need summarization (no summary)
|
|
382
|
+
phases_to_summarize = [p for p in phases if p.summary is None]
|
|
383
|
+
|
|
384
|
+
if previous_analysis and len(phases_to_summarize) < len(phases):
|
|
385
|
+
console.print(f"[cyan]Incremental mode: {len(phases_to_summarize)} phases need summarization "
|
|
386
|
+
f"({len(phases) - len(phases_to_summarize)} already summarized)[/cyan]")
|
|
387
|
+
|
|
252
388
|
with Progress(
|
|
253
389
|
SpinnerColumn(),
|
|
254
390
|
TextColumn("[progress.description]{task.description}"),
|
|
255
391
|
console=console
|
|
256
392
|
) as progress:
|
|
257
|
-
task = progress.add_task("Summarizing phases...", total=len(
|
|
393
|
+
task = progress.add_task("Summarizing phases...", total=len(phases_to_summarize))
|
|
258
394
|
|
|
395
|
+
# Build previous summaries from all phases (including existing ones)
|
|
259
396
|
previous_summaries = []
|
|
260
397
|
for i, phase in enumerate(phases):
|
|
261
|
-
progress.update(task, description=f"
|
|
398
|
+
progress.update(task, description=f"Processing phase {i+1}/{len(phases)}...")
|
|
262
399
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
400
|
+
if phase.summary is None:
|
|
401
|
+
# Need to summarize this phase
|
|
402
|
+
context = summarizer._build_context(previous_summaries)
|
|
403
|
+
summary = summarizer.summarize_phase(phase, context)
|
|
404
|
+
phase.summary = summary
|
|
405
|
+
progress.update(task, advance=1)
|
|
266
406
|
|
|
267
407
|
previous_summaries.append({
|
|
268
408
|
'phase_number': phase.phase_number,
|
|
269
|
-
'summary': summary,
|
|
409
|
+
'summary': phase.summary,
|
|
270
410
|
'loc_delta': phase.loc_delta,
|
|
271
411
|
})
|
|
272
412
|
|
|
273
413
|
summarizer._save_phase_with_summary(phase, str(phases_dir))
|
|
274
|
-
progress.update(task, advance=1)
|
|
275
414
|
|
|
276
|
-
|
|
415
|
+
if len(phases_to_summarize) > 0:
|
|
416
|
+
console.print(f"[green]Summarized {len(phases_to_summarize)} phase(s)[/green]\n")
|
|
417
|
+
else:
|
|
418
|
+
console.print(f"[green]All phases already summarized[/green]\n")
|
|
277
419
|
|
|
278
420
|
# Step 4: Generate global story
|
|
279
421
|
console.print("[bold]Step 4: Generating global narrative...[/bold]")
|
|
@@ -304,9 +446,9 @@ def analyze(repo, output, strategy, chunk_size, max_commits, branch, backend,
|
|
|
304
446
|
OutputWriter.write_markdown(stories, phases, str(markdown_path), repo_name)
|
|
305
447
|
console.print(f"[green]Wrote {markdown_path}[/green]")
|
|
306
448
|
|
|
307
|
-
# Write JSON data
|
|
449
|
+
# Write JSON data with metadata for incremental analysis
|
|
308
450
|
json_path = output_path / "history_data.json"
|
|
309
|
-
OutputWriter.write_json(stories, phases, str(json_path))
|
|
451
|
+
OutputWriter.write_json(stories, phases, str(json_path), repo_path=str(repo_path))
|
|
310
452
|
console.print(f"[green]Wrote {json_path}[/green]")
|
|
311
453
|
|
|
312
454
|
# Write timeline
|
|
@@ -136,7 +136,74 @@ class GitHistoryExtractor:
|
|
|
136
136
|
commits.reverse()
|
|
137
137
|
|
|
138
138
|
# Calculate cumulative LOC
|
|
139
|
-
|
|
139
|
+
return self._calculate_cumulative_loc(commits)
|
|
140
|
+
|
|
141
|
+
def extract_incremental(self, since_commit: str = None, since_date: str = None,
|
|
142
|
+
branch: str = "HEAD") -> List[CommitRecord]:
|
|
143
|
+
"""Extract only new commits since a specific commit or date.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
since_commit: Commit hash to start from (exclusive)
|
|
147
|
+
since_date: ISO date string to start from (exclusive)
|
|
148
|
+
branch: Branch to extract from (default: HEAD)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of CommitRecord objects for new commits, sorted chronologically (oldest first)
|
|
152
|
+
"""
|
|
153
|
+
commits = []
|
|
154
|
+
|
|
155
|
+
# Build revision range
|
|
156
|
+
if since_commit:
|
|
157
|
+
# Extract commits from since_commit..HEAD (exclusive of since_commit)
|
|
158
|
+
revision = f"{since_commit}..{branch}"
|
|
159
|
+
elif since_date:
|
|
160
|
+
# Extract commits after the given date
|
|
161
|
+
commit_iterator = self.repo.iter_commits(
|
|
162
|
+
branch,
|
|
163
|
+
since=since_date
|
|
164
|
+
)
|
|
165
|
+
for commit in commit_iterator:
|
|
166
|
+
try:
|
|
167
|
+
record = self._extract_commit_record(commit)
|
|
168
|
+
commits.append(record)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Warning: Failed to extract commit {commit.hexsha[:8]}: {e}")
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Sort chronologically and calculate LOC
|
|
174
|
+
commits.reverse()
|
|
175
|
+
return self._calculate_cumulative_loc(commits)
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError("Must provide either since_commit or since_date")
|
|
178
|
+
|
|
179
|
+
# Extract commits in the range
|
|
180
|
+
commit_iterator = self.repo.iter_commits(revision)
|
|
181
|
+
|
|
182
|
+
for commit in commit_iterator:
|
|
183
|
+
try:
|
|
184
|
+
record = self._extract_commit_record(commit)
|
|
185
|
+
commits.append(record)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"Warning: Failed to extract commit {commit.hexsha[:8]}: {e}")
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Sort chronologically (oldest first)
|
|
191
|
+
commits.reverse()
|
|
192
|
+
|
|
193
|
+
return commits
|
|
194
|
+
|
|
195
|
+
def _calculate_cumulative_loc(self, commits: List[CommitRecord],
|
|
196
|
+
starting_loc: int = 0) -> List[CommitRecord]:
|
|
197
|
+
"""Calculate cumulative LOC for a list of commits.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
commits: List of CommitRecord objects
|
|
201
|
+
starting_loc: Starting LOC count (for incremental analysis)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Same list with loc_total updated
|
|
205
|
+
"""
|
|
206
|
+
total_loc = starting_loc
|
|
140
207
|
for record in commits:
|
|
141
208
|
total_loc += (record.loc_added - record.loc_deleted)
|
|
142
209
|
record.loc_total = max(0, total_loc)
|
|
@@ -218,11 +285,11 @@ class GitHistoryExtractor:
|
|
|
218
285
|
if not commit.parents:
|
|
219
286
|
# Initial commit
|
|
220
287
|
try:
|
|
221
|
-
diff_index = commit.diff(git.NULL_TREE)
|
|
288
|
+
diff_index = commit.diff(git.NULL_TREE, create_patch=True)
|
|
222
289
|
except:
|
|
223
290
|
return stats
|
|
224
291
|
else:
|
|
225
|
-
diff_index = commit.parents[0].diff(commit)
|
|
292
|
+
diff_index = commit.parents[0].diff(commit, create_patch=True)
|
|
226
293
|
|
|
227
294
|
for diff in diff_index:
|
|
228
295
|
try:
|
|
@@ -239,8 +306,9 @@ class GitHistoryExtractor:
|
|
|
239
306
|
# Get line changes
|
|
240
307
|
if diff.diff:
|
|
241
308
|
diff_text = diff.diff.decode('utf-8', errors='ignore')
|
|
242
|
-
|
|
243
|
-
|
|
309
|
+
# Exclude diff headers (---, +++) from line counts
|
|
310
|
+
insertions = len([l for l in diff_text.split('\n') if l.startswith('+') and not l.startswith('+++')])
|
|
311
|
+
deletions = len([l for l in diff_text.split('\n') if l.startswith('-') and not l.startswith('---')])
|
|
244
312
|
|
|
245
313
|
stats['insertions'] += insertions
|
|
246
314
|
stats['deletions'] += deletions
|
|
@@ -312,9 +380,9 @@ class GitHistoryExtractor:
|
|
|
312
380
|
|
|
313
381
|
try:
|
|
314
382
|
if not commit.parents:
|
|
315
|
-
diff_index = commit.diff(git.NULL_TREE)
|
|
383
|
+
diff_index = commit.diff(git.NULL_TREE, create_patch=True)
|
|
316
384
|
else:
|
|
317
|
-
diff_index = commit.parents[0].diff(commit)
|
|
385
|
+
diff_index = commit.parents[0].diff(commit, create_patch=True)
|
|
318
386
|
|
|
319
387
|
for diff in diff_index:
|
|
320
388
|
if not diff.b_blob:
|
|
@@ -11,6 +11,27 @@ from .chunker import Phase
|
|
|
11
11
|
class OutputWriter:
|
|
12
12
|
"""Write git history stories to various formats."""
|
|
13
13
|
|
|
14
|
+
@staticmethod
|
|
15
|
+
def load_previous_analysis(output_path: str) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Load previous analysis from JSON file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
output_path: Path to output directory containing history_data.json
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dict with previous analysis data, or None if not found
|
|
24
|
+
"""
|
|
25
|
+
json_file = Path(output_path) / "history_data.json"
|
|
26
|
+
if not json_file.exists():
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(json_file, 'r') as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, IOError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
14
35
|
@staticmethod
|
|
15
36
|
def write_markdown(stories: Dict[str, str], phases: List[Phase],
|
|
16
37
|
output_path: str, repo_name: str = "Repository"):
|
|
@@ -164,20 +185,39 @@ class OutputWriter:
|
|
|
164
185
|
f.write("\n")
|
|
165
186
|
|
|
166
187
|
@staticmethod
|
|
167
|
-
def write_json(stories: Dict[str, str], phases: List[Phase], output_path: str
|
|
188
|
+
def write_json(stories: Dict[str, str], phases: List[Phase], output_path: str,
|
|
189
|
+
repo_path: str = None):
|
|
168
190
|
"""
|
|
169
|
-
Write complete data to JSON file.
|
|
191
|
+
Write complete data to JSON file with metadata for incremental analysis.
|
|
170
192
|
|
|
171
193
|
Args:
|
|
172
194
|
stories: Dict of story sections
|
|
173
195
|
phases: List of Phase objects
|
|
174
196
|
output_path: Path to output JSON file
|
|
197
|
+
repo_path: Path to git repository (for metadata)
|
|
175
198
|
"""
|
|
176
199
|
output_file = Path(output_path)
|
|
177
200
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
178
201
|
|
|
179
|
-
|
|
202
|
+
# Calculate metadata from phases
|
|
203
|
+
metadata = {
|
|
180
204
|
'generated_at': datetime.now().isoformat(),
|
|
205
|
+
'total_commits_analyzed': sum(p.commit_count for p in phases),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Add last commit info if phases exist
|
|
209
|
+
if phases:
|
|
210
|
+
last_phase = phases[-1]
|
|
211
|
+
if last_phase.commits:
|
|
212
|
+
last_commit = last_phase.commits[-1]
|
|
213
|
+
metadata['last_commit_hash'] = last_commit.commit_hash
|
|
214
|
+
metadata['last_commit_date'] = last_commit.timestamp
|
|
215
|
+
|
|
216
|
+
if repo_path:
|
|
217
|
+
metadata['repository_path'] = str(Path(repo_path).resolve())
|
|
218
|
+
|
|
219
|
+
data = {
|
|
220
|
+
'metadata': metadata,
|
|
181
221
|
'total_phases': len(phases),
|
|
182
222
|
'total_commits': sum(p.commit_count for p in phases),
|
|
183
223
|
'stories': stories,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|