diff --git a/jupyterlab_git/git.py b/jupyterlab_git/git.py index e2eaedb7d..b8e56701c 100644 --- a/jupyterlab_git/git.py +++ b/jupyterlab_git/git.py @@ -5,7 +5,6 @@ import re import subprocess from urllib.parse import unquote -from codecs import decode, escape_decode import pexpect import tornado @@ -169,14 +168,14 @@ async def changed_files(self, base=None, remote=None, single_commit=None): } """ if single_commit: - cmd = ["git", "diff", "{}^!".format(single_commit), "--name-only"] + cmd = ["git", "diff", "{}^!".format(single_commit), "--name-only", "-z"] elif base and remote: if base == "WORKING": - cmd = ["git", "diff", remote, "--name-only"] + cmd = ["git", "diff", remote, "--name-only", "-z"] elif base == "INDEX": - cmd = ["git", "diff", "--staged", remote, "--name-only"] + cmd = ["git", "diff", "--staged", remote, "--name-only", "-z"] else: - cmd = ["git", "diff", base, remote, "--name-only"] + cmd = ["git", "diff", base, remote, "--name-only", "-z"] else: raise tornado.web.HTTPError( 400, "Either single_commit or (base and remote) must be provided" @@ -194,7 +193,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None): response["command"] = " ".join(cmd) response["message"] = error else: - response["files"] = output.strip().split("\n") + response["files"] = output.strip("\x00").split("\x00") return response @@ -237,7 +236,7 @@ async def status(self, current_path): """ Execute git status command & return the result. """ - cmd = ["git", "status", "--porcelain", "-u"] + cmd = ["git", "status", "--porcelain", "-u", "-z"] code, my_output, my_error = await execute( cmd, cwd=os.path.join(self.root_dir, current_path), ) @@ -250,22 +249,24 @@ async def status(self, current_path): } result = [] - line_array = my_output.splitlines() - for line in line_array: - to1 = None - from_path = line[3:] - if line[0] == "R": - to0 = line[3:].split(" -> ") - to1 = to0[len(to0) - 1] + line_iterable = iter(my_output.strip("\x00").split('\x00')) + result = [] + for line in line_iterable: + x = line[0] + y = line[1] + if line[0]=='R': + #If file was renamed then we need both this line + #and the next line, then we want to move onto the subsequent + #line. We can accomplish this by calling next on the iterable + to = line[3:] + from_path = next(line_iterable) else: - to1 = line[3:] - if to1.startswith('"'): - to1 = to1[1:] - if to1.endswith('"'): - to1 = to1[:-1] - to1 = decode(escape_decode(to1)[0],'utf-8') - from_path = decode(escape_decode(from_path)[0],'utf-8') - result.append({"x": line[0], "y": line[1], "to": to1, "from": from_path}) + #to and from_path are the same + from_path = line[3:] + to = line[3:] + result.append({"x": x, "y": y, "to": to, "from": from_path}) + + return {"code": code, "files": result} async def log(self, current_path, history_count=10): @@ -314,10 +315,10 @@ async def log(self, current_path, history_count=10): async def detailed_log(self, selected_hash, current_path): """ - Execute git log -1 --stat --numstat --oneline command (used to get + Execute git log -1 --stat --numstat --oneline -z command (used to get insertions & deletions per file) & return the result. """ - cmd = ["git", "log", "-1", "--stat", "--numstat", "--oneline", selected_hash] + cmd = ["git", "log", "-1", "--stat", "--numstat", "--oneline", "-z", selected_hash] code, my_output, my_error = await execute( cmd, cwd=os.path.join(self.root_dir, current_path), ) @@ -329,7 +330,7 @@ async def detailed_log(self, selected_hash, current_path): note = [0] * 3 count = 0 temp = "" - line_array = my_output.splitlines() + line_array = re.split("\x00|\n|\r\n|\r", my_output) length = len(line_array) INSERTION_INDEX = 0 DELETION_INDEX = 1 @@ -342,7 +343,7 @@ async def detailed_log(self, selected_hash, current_path): note[count] = words[i] count += 1 for num in range(1, int(length / 2)): - line_info = line_array[num].split(maxsplit=2) + line_info = line_array[num].split('\t', maxsplit=2) words = line_info[2].split("/") length = len(words) result.append( @@ -373,14 +374,14 @@ async def diff(self, top_repo_path): """ Execute git diff command & return the result. """ - cmd = ["git", "diff", "--numstat"] + cmd = ["git", "diff", "--numstat", "-z"] code, my_output, my_error = await execute(cmd, cwd=top_repo_path) if code != 0: return {"code": code, "command": " ".join(cmd), "message": my_error} result = [] - line_array = my_output.splitlines() + line_array = my_output.strip('\x00').split('\x00') for line in line_array: linesplit = line.split() result.append( diff --git a/jupyterlab_git/tests/test_detailed_log.py b/jupyterlab_git/tests/test_detailed_log.py index 8d6f6addc..443b51145 100644 --- a/jupyterlab_git/tests/test_detailed_log.py +++ b/jupyterlab_git/tests/test_detailed_log.py @@ -15,28 +15,35 @@ async def test_detailed_log(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given - process_output = [ + process_output_first_half = [ "f29660a (HEAD, origin/feature) Commit message", - "10 3 notebook_without_spaces.ipynb", - "11 4 Notebook with spaces.ipynb", - "12 5 path/notebook_without_spaces.ipynb", - "13 6 path/Notebook with spaces.ipynb", + "10\t3\tnotebook_without_spaces.ipynb", + "11\t4\tNotebook with spaces.ipynb", + "12\t5\tpath/notebook_without_spaces.ipynb", + "13\t6\tpath/Notebook with spaces.ipynb", + "14\t1\tpath/Notebook with λ.ipynb", + ] + process_output_second_half = [ " notebook_without_spaces.ipynb | 13 ++++++++---", " Notebook with spaces.ipynb | 15 +++++++++----", " path/notebook_without_spaces.ipynb | 17 ++++++++++-----", " path/Notebook with spaces.ipynb | 19 +++++++++++------", - " 4 files changed, 46 insertions(+), 18 deletions(-)", + " path/Notebook with \\316\\273.ipynb | 15 +++++++++++-", + " 5 files changed, 50 insertions(+), 19 deletions(-)", ] - mock_execute.return_value = tornado.gen.maybe_future( - (0, "\n".join(process_output), "") + process_output_first_half = "\x00".join(process_output_first_half) + process_output_second_half = "\n".join(process_output_second_half) + process_output = process_output_first_half + "\x00" + process_output_second_half + mock_execute._mock_return_value = tornado.gen.maybe_future( + (0, process_output, "") ) expected_response = { "code": 0, - "modified_file_note": " 4 files changed, 46 insertions(+), 18 deletions(-)", - "modified_files_count": "4", - "number_of_insertions": "46", - "number_of_deletions": "18", + "modified_file_note": " 5 files changed, 50 insertions(+), 19 deletions(-)", + "modified_files_count": "5", + "number_of_insertions": "50", + "number_of_deletions": "19", "modified_files": [ { "modified_file_path": "notebook_without_spaces.ipynb", @@ -62,6 +69,12 @@ async def test_detailed_log(): "insertion": "13", "deletion": "6", }, + { + "modified_file_path": "path/Notebook with λ.ipynb", + "modified_file_name": "Notebook with λ.ipynb", + "insertion": "14", + "deletion": "1", + }, ], } @@ -83,6 +96,7 @@ async def test_detailed_log(): "--stat", "--numstat", "--oneline", + "-z", "f29660a2472e24164906af8653babeb48e4bf2ab", ], cwd=os.path.join("/bin", "test_curr_path"), diff --git a/jupyterlab_git/tests/test_diff.py b/jupyterlab_git/tests/test_diff.py index 5496c44a6..046780759 100644 --- a/jupyterlab_git/tests/test_diff.py +++ b/jupyterlab_git/tests/test_diff.py @@ -24,7 +24,7 @@ async def test_changed_files_single_commit(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given mock_execute.return_value = tornado.gen.maybe_future( - (0, "file1.ipynb\nfile2.py", "") + (0, "file1.ipynb\x00file2.py", "") ) # When @@ -39,6 +39,7 @@ async def test_changed_files_single_commit(): "diff", "64950a634cd11d1a01ddfedaeffed67b531cb11e^!", "--name-only", + "-z", ], cwd="/bin", ) @@ -50,7 +51,7 @@ async def test_changed_files_working_tree(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given mock_execute.return_value = tornado.gen.maybe_future( - (0, "file1.ipynb\nfile2.py", "") + (0, "file1.ipynb\x00file2.py", "") ) # When @@ -60,7 +61,7 @@ async def test_changed_files_working_tree(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "HEAD", "--name-only"], cwd="/bin" + ["git", "diff", "HEAD", "--name-only", "-z"], cwd="/bin" ) assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response @@ -70,7 +71,7 @@ async def test_changed_files_index(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given mock_execute.return_value = tornado.gen.maybe_future( - (0, "file1.ipynb\nfile2.py", "") + (0, "file1.ipynb\x00file2.py", "") ) # When @@ -80,7 +81,7 @@ async def test_changed_files_index(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "--staged", "HEAD", "--name-only"], cwd="/bin" + ["git", "diff", "--staged", "HEAD", "--name-only", "-z"], cwd="/bin" ) assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response @@ -90,7 +91,7 @@ async def test_changed_files_two_commits(): with patch("jupyterlab_git.git.execute") as mock_execute: # Given mock_execute.return_value = tornado.gen.maybe_future( - (0, "file1.ipynb\nfile2.py", "") + (0, "file1.ipynb\x00file2.py", "") ) # When @@ -100,7 +101,7 @@ async def test_changed_files_two_commits(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "HEAD", "origin/HEAD", "--name-only"], cwd="/bin" + ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin" ) assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response @@ -118,6 +119,6 @@ async def test_changed_files_git_diff_error(): # Then mock_execute.assert_called_once_with( - ["git", "diff", "HEAD", "origin/HEAD", "--name-only"], cwd="/bin" + ["git", "diff", "HEAD", "origin/HEAD", "--name-only", "-z"], cwd="/bin" ) assert {"code": 128, "message": "error message"} == actual_response diff --git a/jupyterlab_git/tests/test_status.py b/jupyterlab_git/tests/test_status.py new file mode 100644 index 000000000..4d6b407c0 --- /dev/null +++ b/jupyterlab_git/tests/test_status.py @@ -0,0 +1,46 @@ +# python lib +import os +from unittest.mock import Mock, call, patch + +import pytest +import tornado + +# local lib +from jupyterlab_git.git import Git + +from .testutils import FakeContentManager + +@pytest.mark.asyncio +async def test_status(): + with patch("jupyterlab_git.git.execute") as mock_execute: + # Given + process_output = ( + "A notebook with spaces.ipynb", + "M notebook with λ.ipynb", + "R renamed_to_θ.py", + "originally_named_π.py", + "?? untracked.ipynb", + ) + + expected_resonse = [ + {"x": "A", "y": " ", "to": "notebook with spaces.ipynb", "from": "notebook with spaces.ipynb"}, + {"x": "M", "y": " ", "to": "notebook with λ.ipynb", "from": "notebook with λ.ipynb"}, + {"x": "R", "y": " ", "to": "renamed_to_θ.py", "from": "originally_named_π.py"}, + {"x": "?", "y": "?", "to": "untracked.ipynb", "from": "untracked.ipynb"}, + ] + mock_execute.return_value = tornado.gen.maybe_future( + (0, "\x00".join(process_output), "") + + ) + + # When + actual_response = await Git(FakeContentManager("/bin")).status( + current_path="test_curr_path" + ) + + # Then + mock_execute.assert_called_once_with( + ["git", "status", "--porcelain" , "-u", "-z"], cwd="/bin/test_curr_path" + ) + + assert {"code": 0, "files": expected_resonse} == actual_response