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.
Files changed (32) hide show
  1. {gitview-0.1.1/gitview.egg-info → gitview-0.1.2}/PKG-INFO +1 -1
  2. {gitview-0.1.1 → gitview-0.1.2}/gitview/__init__.py +1 -1
  3. {gitview-0.1.1 → gitview-0.1.2}/gitview/cli.py +157 -15
  4. {gitview-0.1.1 → gitview-0.1.2}/gitview/extractor.py +75 -7
  5. {gitview-0.1.1 → gitview-0.1.2}/gitview/writer.py +43 -3
  6. {gitview-0.1.1 → gitview-0.1.2/gitview.egg-info}/PKG-INFO +1 -1
  7. {gitview-0.1.1 → gitview-0.1.2}/pyproject.toml +1 -1
  8. {gitview-0.1.1 → gitview-0.1.2}/INSTALL.md +0 -0
  9. {gitview-0.1.1 → gitview-0.1.2}/LICENSE +0 -0
  10. {gitview-0.1.1 → gitview-0.1.2}/MANIFEST.in +0 -0
  11. {gitview-0.1.1 → gitview-0.1.2}/README.md +0 -0
  12. {gitview-0.1.1 → gitview-0.1.2}/bin/gitview +0 -0
  13. {gitview-0.1.1 → gitview-0.1.2}/examples/basic_usage.py +0 -0
  14. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/__init__.py +0 -0
  15. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/anthropic_backend.py +0 -0
  16. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/base.py +0 -0
  17. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/ollama_backend.py +0 -0
  18. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/openai_backend.py +0 -0
  19. {gitview-0.1.1 → gitview-0.1.2}/gitview/backends/router.py +0 -0
  20. {gitview-0.1.1 → gitview-0.1.2}/gitview/chunker.py +0 -0
  21. {gitview-0.1.1 → gitview-0.1.2}/gitview/storyteller.py +0 -0
  22. {gitview-0.1.1 → gitview-0.1.2}/gitview/summarizer.py +0 -0
  23. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/SOURCES.txt +0 -0
  24. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/dependency_links.txt +0 -0
  25. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/entry_points.txt +0 -0
  26. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/not-zip-safe +0 -0
  27. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/requires.txt +0 -0
  28. {gitview-0.1.1 → gitview-0.1.2}/gitview.egg-info/top_level.txt +0 -0
  29. {gitview-0.1.1 → gitview-0.1.2}/requirements.txt +0 -0
  30. {gitview-0.1.1 → gitview-0.1.2}/setup.cfg +0 -0
  31. {gitview-0.1.1 → gitview-0.1.2}/setup.py +0 -0
  32. {gitview-0.1.1 → gitview-0.1.2}/verify_installation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitview
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Git history analyzer with LLM-powered narrative generation
5
5
  Home-page: https://github.com/carstenbund/gitview
6
6
  Author: GitView Contributors
@@ -1,3 +1,3 @@
1
1
  """GitView - Git history analyzer with LLM-powered narrative generation."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -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
- records = extractor.extract_history(max_commits=max_commits, branch=branch)
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
- console.print(f"[green]Extracted {len(records)} commits[/green]\n")
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
- phases = chunker.chunk(records, **kwargs)
227
- console.print(f"[green]Created {len(phases)} phases[/green]\n")
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(phases))
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"Summarizing phase {i+1}/{len(phases)}...")
398
+ progress.update(task, description=f"Processing phase {i+1}/{len(phases)}...")
262
399
 
263
- context = summarizer._build_context(previous_summaries)
264
- summary = summarizer.summarize_phase(phase, context)
265
- phase.summary = summary
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
- console.print(f"[green]Summarized all phases[/green]\n")
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
- total_loc = 0
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
- insertions = len([l for l in diff_text.split('\n') if l.startswith('+')])
243
- deletions = len([l for l in diff_text.split('\n') if l.startswith('-')])
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
- data = {
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitview
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Git history analyzer with LLM-powered narrative generation
5
5
  Home-page: https://github.com/carstenbund/gitview
6
6
  Author: GitView Contributors
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitview"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Git history analyzer with LLM-powered narrative generation"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
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