diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 310305a..1994a9a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,3 +22,13 @@ jobs: python-version: ${{ matrix.python-version }} - name: Run tests run: python -m unittest discover -s tests -v + + windows-paths: + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.8' + - name: Run tests (including subst drive) + run: python -m unittest discover -s tests -v diff --git a/sublime_github.py b/sublime_github.py index 946850d..ba6f968 100644 --- a/sublime_github.py +++ b/sublime_github.py @@ -390,11 +390,25 @@ def done_remote(self, result): # Get the repo's explicit toplevel path def done_toplevel(self, result): self.toplevel_path = result.strip() - # get file path within repo + # Preserve the opened filename (including symlinks) whenever its path is already relative to Git's root. absolute_path = self.view.file_name() - # self.view.file_name() contains backslash on Windows instead of forwardslash - absolute_path = absolute_path.replace('\\', '/') - relative_path = "/" + os.path.relpath(absolute_path, self.toplevel_path).replace('\\', '/') + try: + relative_path = os.path.relpath(absolute_path, self.toplevel_path) + except ValueError: # Different drives, e.g. a Windows subst drive vs Git's physical path. + relative_path = None + if relative_path is None or relative_path == os.pardir or relative_path.startswith(os.pardir + os.sep): + # Resolve only the parent directory so a symlinked file keeps its name in the GitHub URL. + parent = os.path.realpath(os.path.dirname(absolute_path)) + root = os.path.realpath(self.toplevel_path) + try: + relative_path = os.path.relpath(os.path.join(parent, os.path.basename(absolute_path)), root) + except ValueError: + sublime.error_message("Cannot locate this file within the Git repository.") + return + if relative_path == os.pardir or relative_path.startswith(os.pardir + os.sep): + sublime.error_message("This file is outside the Git repository.") + return + relative_path = "/" + relative_path.replace('\\', '/') line_nums = "" if self.allows_line_highlights: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index bda47c3..f519d4a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -201,6 +201,67 @@ def test_git_helper_runs_in_file_directory_without_changing_process_cwd(self): self.assertEqual([os.path.realpath(path) for path in results], [os.path.realpath(directory)]) self.assertEqual(os.getcwd(), original_cwd) + @unittest.skipIf(os.name == "nt", "creating symlinks requires Windows developer permissions") + def test_repository_link_keeps_symlink_filename(self): + with tempfile.TemporaryDirectory() as directory: + repo = Path(os.path.realpath(directory)) / "repo" + repo.mkdir() + (repo / "target.py").touch() + link = repo / "link.py" + link.symlink_to("target.py") + command = self.plugin.OpenRemoteUrlCommand() + command.view = types.SimpleNamespace(file_name=lambda: str(link), sel=lambda: []) + command.repo_url = "https://github.com/Giftly/China" + command.remote_branch = "main" + command.settings = types.SimpleNamespace(get=lambda key: None) + command.done_toplevel(str(repo)) + self.assertEqual(self.opened_url, "https://github.com/Giftly/China/blob/main/link.py") + + @unittest.skipIf(os.name == "nt", "creating symlinks requires Windows developer permissions") + def test_repository_link_resolves_directory_alias_but_keeps_file_symlink(self): + with tempfile.TemporaryDirectory() as directory: + directory = os.path.realpath(directory) + repo = Path(directory) / "repo" + repo.mkdir() + (repo / "target.py").touch() + (repo / "link.py").symlink_to("target.py") + alias = Path(directory) / "alias" + alias.symlink_to(repo, target_is_directory=True) + command = self.plugin.OpenRemoteUrlCommand() + command.view = types.SimpleNamespace(file_name=lambda: str(alias / "link.py"), sel=lambda: []) + command.repo_url = "https://github.com/Giftly/China" + command.remote_branch = "main" + command.settings = types.SimpleNamespace(get=lambda key: None) + command.done_toplevel(str(repo)) + self.assertEqual(self.opened_url, "https://github.com/Giftly/China/blob/main/link.py") + + @unittest.skipUnless(os.name == "nt", "requires Windows subst drives") + def test_repository_link_on_subst_drive(self): + with tempfile.TemporaryDirectory() as directory: + repo = Path(directory) / "repo" + repo.mkdir() + (repo / "file.py").touch() + drive = next(letter for letter in "ZYXWVUTSRQPONMLKJ" if not os.path.exists(letter + ":\\")) + subprocess.run(["subst", drive + ":", directory], check=True) + try: + mapped_repo = drive + ":\\repo" + subprocess.run(["git", "init", "-q", mapped_repo], check=True) + git_root = subprocess.check_output( + ["git", "-C", mapped_repo, "rev-parse", "--show-toplevel"], text=True, + ).strip() + command = self.plugin.OpenRemoteUrlCommand() + command.view = types.SimpleNamespace(file_name=lambda: mapped_repo + "\\file.py", sel=lambda: []) + command.repo_url = "https://github.com/Giftly/China" + command.remote_branch = "main" + command.settings = types.SimpleNamespace(get=lambda key: None) + command.done_toplevel(git_root) + self.assertEqual(self.opened_url, "https://github.com/Giftly/China/blob/main/file.py") + # Also exercise the physical path Git may return when using subst. + command.done_toplevel(str(repo)) + self.assertEqual(self.opened_url, "https://github.com/Giftly/China/blob/main/file.py") + finally: + subprocess.run(["subst", drive + ":", "/D"], check=True) + def test_view_and_blame_detect_default_branch_and_include_selected_lines(self): class Region: def begin(self): return 9