@tanagram/cli 0.4.10 → 0.4.11

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.
package/git/diff.go CHANGED
@@ -66,6 +66,40 @@ func GetAllChanges() (*DiffResult, error) {
66
66
  return result, nil
67
67
  }
68
68
 
69
+ // DiffFiles compares two files using git diff --no-index
70
+ func DiffFiles(oldFile, newFile, displayPath string) (*DiffResult, error) {
71
+ cmd := exec.Command("git", "diff", "--no-index", "--unified=0", oldFile, newFile)
72
+ output, err := cmd.Output()
73
+
74
+ // git diff --no-index returns exit code 1 if there are differences
75
+ // It returns 0 if identical
76
+ if err != nil {
77
+ if exitError, ok := err.(*exec.ExitError); ok {
78
+ if exitError.ExitCode() != 1 {
79
+ // Anything other than 0 or 1 is an actual error
80
+ return nil, fmt.Errorf("failed to run git diff: %w", err)
81
+ }
82
+ // Exit code 1 means differences found, proceed to parse
83
+ } else {
84
+ return nil, fmt.Errorf("failed to run git diff: %w", err)
85
+ }
86
+ }
87
+
88
+ // Parse the diff output
89
+ result, err := parseDiff(string(output))
90
+ if err != nil {
91
+ return nil, err
92
+ }
93
+
94
+ // Fix up the file paths in the result to match the displayPath
95
+ // git diff --no-index outputs temp file paths like "b/tmp/..."
96
+ for i := range result.Changes {
97
+ result.Changes[i].File = displayPath
98
+ }
99
+
100
+ return result, nil
101
+ }
102
+
69
103
  // parseDiff parses unified diff format and extracts changed lines
70
104
  func parseDiff(diffText string) (*DiffResult, error) {
71
105
  result := &DiffResult{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@ import (
12
12
  "strings"
13
13
  "time"
14
14
 
15
+ "github.com/tanagram/cli/git"
15
16
  "github.com/tanagram/cli/storage"
16
17
  )
17
18
 
@@ -21,6 +22,7 @@ type FileState struct {
21
22
  Hash string `json:"hash"`
22
23
  Size int64 `json:"size"`
23
24
  ModifiedTime time.Time `json:"modified_time"`
25
+ Content string `json:"content,omitempty"`
24
26
  }
25
27
 
26
28
  // Snapshot represents a snapshot of the working directory
@@ -179,11 +181,18 @@ func CreateOptimized(gitRoot string, targetFiles []string) error {
179
181
  continue
180
182
  }
181
183
 
184
+ // Read content
185
+ contentBytes, err := os.ReadFile(fullPath)
186
+ if err != nil {
187
+ continue
188
+ }
189
+
182
190
  snapshot.Files[relPath] = &FileState{
183
191
  Path: relPath,
184
192
  Hash: hash,
185
193
  Size: info.Size(),
186
194
  ModifiedTime: info.ModTime(),
195
+ Content: string(contentBytes),
187
196
  }
188
197
  }
189
198
 
@@ -490,31 +499,113 @@ type ChangedLine struct {
490
499
  func GetChangedLinesForChecker(gitRoot string, snapshot *Snapshot, compareResult *CompareResult) ([]ChangedLine, error) {
491
500
  var changes []ChangedLine
492
501
 
493
- // For each modified file, include all non-empty lines
494
- // (we can't easily determine which specific lines changed without doing a full diff)
502
+ // For modified files, we diff against the snapshot version
495
503
  for _, relPath := range compareResult.ModifiedFiles {
496
504
  fullPath := filepath.Join(gitRoot, relPath)
497
505
 
498
- // Read new content
506
+ // Get New Content (Current)
499
507
  newData, err := os.ReadFile(fullPath)
500
508
  if err != nil {
501
509
  continue
502
510
  }
503
-
504
- // Skip binary files
505
511
  if !isText(newData) {
506
512
  continue
507
513
  }
508
514
 
509
- newLines := strings.Split(string(newData), "\n")
515
+ // Get Old Content (Snapshot)
516
+ var oldData []byte
517
+
518
+ if fileState, ok := snapshot.Files[relPath]; ok && fileState.Content != "" {
519
+ // Was dirty -> use stored content
520
+ oldData = []byte(fileState.Content)
521
+ } else {
522
+ // Was clean -> use git show
523
+ // If it wasn't in snapshot.Files but is in ModifiedFiles, it must have been clean at HEAD
524
+ // and now is modified.
525
+ // Note: If snapshot.Files has it but Content is empty, that means it was clean but we stored it?
526
+ // No, CreateOptimized only stores Content for dirty files.
527
+ // But wait, CreateOptimized only adds files to snapshot.Files if they are dirty or targetted.
528
+ // If a file is NOT in snapshot.Files, it is assumed clean at HeadCommit.
529
+
530
+ // However, compareResult.ModifiedFiles includes files that:
531
+ // 1. Were in snapshot (hash changed)
532
+ // 2. Were NOT in snapshot (current hash != HEAD hash)
533
+
534
+ // Case 1: In snapshot
535
+ if fileState, ok := snapshot.Files[relPath]; ok {
536
+ if fileState.Content != "" {
537
+ oldData = []byte(fileState.Content)
538
+ } else {
539
+ // It was in snapshot but no content? This happens if it was > 1MB
540
+ // or if we used the legacy Create method which doesn't store content.
541
+ // In that case we fall back to reading from HEAD (imperfect but best effort)
542
+ // OR we just treat all lines as changed (current behavior) if we can't get old content.
543
+
544
+ // Try to get from HEAD just in case it matches
545
+ hash, _ := getHashAtCommit(gitRoot, snapshot.HeadCommit, relPath)
546
+ if hash == fileState.Hash {
547
+ // It matches HEAD, so we can get content from HEAD
548
+ out, err := runGit(gitRoot, "show", fmt.Sprintf("%s:%s", snapshot.HeadCommit, relPath))
549
+ if err == nil {
550
+ oldData = []byte(out)
551
+ }
552
+ }
553
+ }
554
+ } else {
555
+ // Case 2: Not in snapshot (was clean at HEAD)
556
+ out, err := runGit(gitRoot, "show", fmt.Sprintf("%s:%s", snapshot.HeadCommit, relPath))
557
+ if err == nil {
558
+ oldData = []byte(out)
559
+ }
560
+ }
561
+ }
562
+
563
+ // If we couldn't get old data, fallback to treating all lines as new (safest)
564
+ if len(oldData) == 0 {
565
+ newLines := strings.Split(string(newData), "\n")
566
+ for i, line := range newLines {
567
+ if strings.TrimSpace(line) != "" {
568
+ changes = append(changes, ChangedLine{
569
+ File: relPath,
570
+ LineNumber: i + 1,
571
+ Content: line,
572
+ ChangeType: "+",
573
+ })
574
+ }
575
+ }
576
+ continue
577
+ }
510
578
 
511
- // Include all non-empty lines from modified files
512
- for i, line := range newLines {
513
- if strings.TrimSpace(line) != "" {
579
+ // Do the diff
580
+ // Write to temp files
581
+ tmpOld, err := os.CreateTemp("", "tanagram-old-*")
582
+ if err != nil {
583
+ continue
584
+ }
585
+ defer os.Remove(tmpOld.Name())
586
+ tmpOld.Write(oldData)
587
+ tmpOld.Close()
588
+
589
+ tmpNew, err := os.CreateTemp("", "tanagram-new-*")
590
+ if err != nil {
591
+ continue
592
+ }
593
+ defer os.Remove(tmpNew.Name())
594
+ tmpNew.Write(newData)
595
+ tmpNew.Close()
596
+
597
+ diffResult, err := git.DiffFiles(tmpOld.Name(), tmpNew.Name(), relPath)
598
+ if err != nil {
599
+ continue
600
+ }
601
+
602
+ // Convert git.ChangedLine to snapshot.ChangedLine
603
+ for _, c := range diffResult.Changes {
604
+ if c.ChangeType == "+" {
514
605
  changes = append(changes, ChangedLine{
515
606
  File: relPath,
516
- LineNumber: i + 1,
517
- Content: line,
607
+ LineNumber: c.LineNumber,
608
+ Content: c.Content,
518
609
  ChangeType: "+",
519
610
  })
520
611
  }