@pixelbyte-software/pixcode 1.53.0 → 1.53.2

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.
@@ -380,6 +380,27 @@ function normalizeRepositoryRelativeFilePath(filePath) {
380
380
  .trim();
381
381
  }
382
382
 
383
+ function normalizeRequestedRepositoryFilePath(repositoryRootPath, filePath) {
384
+ const rawFilePath = String(filePath || '').replace(/\\/g, '/').trim();
385
+ if (!rawFilePath) {
386
+ return '';
387
+ }
388
+
389
+ if (path.isAbsolute(rawFilePath)) {
390
+ const resolvedFilePath = path.resolve(rawFilePath);
391
+ const resolvedRepositoryRoot = path.resolve(repositoryRootPath);
392
+ const relativePath = path.relative(resolvedRepositoryRoot, resolvedFilePath);
393
+
394
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
395
+ throw new Error('Invalid file path: outside repository');
396
+ }
397
+
398
+ return normalizeRepositoryRelativeFilePath(relativePath);
399
+ }
400
+
401
+ return normalizeRepositoryRelativeFilePath(rawFilePath);
402
+ }
403
+
383
404
  function parseStatusFilePaths(statusOutput) {
384
405
  return statusOutput
385
406
  .split('\n')
@@ -394,7 +415,7 @@ function parseStatusFilePaths(statusOutput) {
394
415
  }
395
416
 
396
417
  function buildFilePathCandidates(projectPath, repositoryRootPath, filePath) {
397
- const normalizedFilePath = normalizeRepositoryRelativeFilePath(filePath);
418
+ const normalizedFilePath = normalizeRequestedRepositoryFilePath(repositoryRootPath, filePath);
398
419
  const projectRelativePath = normalizeRepositoryRelativeFilePath(path.relative(repositoryRootPath, projectPath));
399
420
  const candidates = [normalizedFilePath];
400
421
 
@@ -629,53 +650,93 @@ router.get('/file-with-diff', async (req, res) => {
629
650
  repositoryRelativeFilePath,
630
651
  } = await resolveRepositoryFilePath(projectPath, file);
631
652
 
653
+ const readFileFromIndex = async () => {
654
+ const { stdout } = await spawnAsync(
655
+ 'git',
656
+ ['show', `:${repositoryRelativeFilePath}`],
657
+ { cwd: repositoryRootPath },
658
+ );
659
+ return stdout;
660
+ };
661
+ const readFileFromHead = async () => {
662
+ const { stdout } = await spawnAsync(
663
+ 'git',
664
+ ['show', `HEAD:${repositoryRelativeFilePath}`],
665
+ { cwd: repositoryRootPath },
666
+ );
667
+ return stdout;
668
+ };
669
+
632
670
  // Check file status
633
671
  const { stdout: statusOutput } = await spawnAsync(
634
672
  'git',
635
673
  ['status', '--porcelain', '--', repositoryRelativeFilePath],
636
674
  { cwd: repositoryRootPath },
637
675
  );
638
- const isUntracked = statusOutput.startsWith('??');
639
- const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
676
+ const statusLine = statusOutput.split('\n').find((line) => line.trim()) || '';
677
+ const indexStatus = statusLine[0] || ' ';
678
+ const worktreeStatus = statusLine[1] || ' ';
679
+ const isUntracked = statusLine.startsWith('??');
680
+ const hasUnstagedChange = !isUntracked && worktreeStatus !== ' ';
681
+ const hasStagedChange = !isUntracked && indexStatus !== ' ';
682
+ const isDeleted = worktreeStatus === 'D' || (!hasUnstagedChange && indexStatus === 'D');
640
683
 
641
684
  let currentContent = '';
642
685
  let oldContent = '';
643
686
 
644
- if (isDeleted) {
645
- // For deleted files, get content from HEAD
646
- const { stdout: headContent } = await spawnAsync(
647
- 'git',
648
- ['show', `HEAD:${repositoryRelativeFilePath}`],
649
- { cwd: repositoryRootPath },
650
- );
651
- oldContent = headContent;
652
- currentContent = headContent; // Show the deleted content in editor
653
- } else {
654
- // Get current file content
687
+ if (hasUnstagedChange) {
688
+ try {
689
+ oldContent = await readFileFromIndex();
690
+ } catch {
691
+ oldContent = '';
692
+ }
693
+
694
+ if (worktreeStatus === 'D') {
695
+ currentContent = '';
696
+ } else {
697
+ const filePath = path.join(repositoryRootPath, repositoryRelativeFilePath);
698
+ const stats = await fs.stat(filePath);
699
+ if (stats.isDirectory()) {
700
+ return res.status(400).json({ error: 'Cannot show diff for directories' });
701
+ }
702
+ currentContent = await fs.readFile(filePath, 'utf-8');
703
+ }
704
+ } else if (hasStagedChange) {
705
+ try {
706
+ oldContent = await readFileFromHead();
707
+ } catch {
708
+ // A staged add has no HEAD copy; showing the whole file as added is correct.
709
+ oldContent = '';
710
+ }
711
+
712
+ if (indexStatus === 'D') {
713
+ currentContent = '';
714
+ } else {
715
+ try {
716
+ currentContent = await readFileFromIndex();
717
+ } catch {
718
+ currentContent = '';
719
+ }
720
+ }
721
+ } else if (isUntracked) {
655
722
  const filePath = path.join(repositoryRootPath, repositoryRelativeFilePath);
656
723
  const stats = await fs.stat(filePath);
657
724
 
658
725
  if (stats.isDirectory()) {
659
- // Cannot show content for directories
660
726
  return res.status(400).json({ error: 'Cannot show diff for directories' });
661
727
  }
662
728
 
663
729
  currentContent = await fs.readFile(filePath, 'utf-8');
730
+ } else {
731
+ const filePath = path.join(repositoryRootPath, repositoryRelativeFilePath);
732
+ const stats = await fs.stat(filePath);
664
733
 
665
- if (!isUntracked) {
666
- // Get the old content from HEAD for tracked files
667
- try {
668
- const { stdout: headContent } = await spawnAsync(
669
- 'git',
670
- ['show', `HEAD:${repositoryRelativeFilePath}`],
671
- { cwd: repositoryRootPath },
672
- );
673
- oldContent = headContent;
674
- } catch (error) {
675
- // File might be newly added to git (staged but not committed)
676
- oldContent = '';
677
- }
734
+ if (stats.isDirectory()) {
735
+ return res.status(400).json({ error: 'Cannot show diff for directories' });
678
736
  }
737
+
738
+ currentContent = await fs.readFile(filePath, 'utf-8');
739
+ oldContent = currentContent;
679
740
  }
680
741
 
681
742
  res.json({