Skip to content

Commit f669aee

Browse files
Merge pull request #364 from jaipreet-s/change-files-api
Server API to list changed files in Git
2 parents c2c43d6 + 4b71390 commit f669aee

File tree

3 files changed

+161
-2
lines changed

3 files changed

+161
-2
lines changed

jupyterlab_git/git.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"""
44
import os
55
import subprocess
6-
from subprocess import Popen, PIPE
6+
from subprocess import Popen, PIPE, CalledProcessError
77
from urllib.parse import unquote
88

9+
from tornado.web import HTTPError
10+
911

1012
class Git:
1113
"""
@@ -16,6 +18,55 @@ def __init__(self, root_dir, *args, **kwargs):
1618
super(Git, self).__init__(*args, **kwargs)
1719
self.root_dir = os.path.realpath(os.path.expanduser(root_dir))
1820

21+
22+
def changed_files(self, base=None, remote=None, single_commit=None):
23+
"""Gets the list of changed files between two Git refs, or the files changed in a single commit
24+
25+
There are two reserved "refs" for the base
26+
1. WORKING : Represents the Git working tree
27+
2. INDEX: Represents the Git staging area / index
28+
29+
Keyword Arguments:
30+
single_commit {string} -- The single commit ref
31+
base {string} -- the base Git ref
32+
remote {string} -- the remote Git ref
33+
34+
Returns:
35+
dict -- the response of format {
36+
"code": int, # Command status code
37+
"files": [string, string], # List of files changed.
38+
"message": [string] # Error response
39+
}
40+
"""
41+
if single_commit:
42+
cmd = ['git', 'diff', f'{single_commit}^!', '--name-only']
43+
elif base and remote:
44+
if base == 'WORKING':
45+
cmd = ['git', 'diff', remote, '--name-only']
46+
elif base == 'INDEX':
47+
cmd = ['git', 'diff', '--staged', remote, '--name-only']
48+
else:
49+
cmd = ['git', 'diff', base, remote, '--name-only']
50+
else:
51+
raise HTTPError(400, f'Either single_commit or (base and remote) must be provided')
52+
53+
54+
response = {}
55+
try:
56+
stdout = subprocess.check_output(
57+
cmd,
58+
cwd=self.root_dir,
59+
stderr=subprocess.STDOUT
60+
)
61+
response['files'] = stdout.decode('utf-8').strip().split('\n')
62+
response['code'] = 0
63+
except CalledProcessError as e:
64+
response['message'] = e.output.decode('utf-8')
65+
response['code'] = e.returncode
66+
67+
return response
68+
69+
1970
def clone(self, current_path, repo_url):
2071
"""
2172
Execute `git clone`. Disables prompts for the password to avoid the terminal hanging.

jupyterlab_git/handlers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,15 @@ def post(self):
413413
print(my_output)
414414
self.finish(my_output)
415415

416+
class GitChangedFilesHandler(GitHandler):
417+
418+
def post(self):
419+
self.finish(
420+
json.dumps(
421+
self.git.changed_files(**self.get_json_body())
422+
)
423+
)
424+
416425

417426
def setup_handlers(web_app):
418427
"""
@@ -440,7 +449,8 @@ def setup_handlers(web_app):
440449
("/git/all_history", GitAllHistoryHandler),
441450
("/git/add_all_untracked", GitAddAllUntrackedHandler),
442451
("/git/clone", GitCloneHandler),
443-
("/git/upstream", GitUpstreamHandler)
452+
("/git/upstream", GitUpstreamHandler),
453+
("/git/changed_files", GitChangedFilesHandler)
444454
]
445455

446456
# add the baseurl to our paths

tests/unit/test_diff.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from subprocess import PIPE, STDOUT, CalledProcessError
2+
3+
from mock import Mock, call, patch
4+
import pytest
5+
from tornado.web import HTTPError
6+
7+
8+
from jupyterlab_git.git import Git
9+
10+
11+
def test_changed_files_invalid_input():
12+
with pytest.raises(HTTPError):
13+
actual_response = Git(root_dir="/bin").changed_files(
14+
base="64950a634cd11d1a01ddfedaeffed67b531cb11e"
15+
)
16+
17+
18+
@patch("subprocess.check_output")
19+
def test_changed_files_single_commit(mock_call):
20+
# Given
21+
mock_call.return_value = b"file1.ipynb\nfile2.py"
22+
23+
# When
24+
actual_response = Git(root_dir="/bin").changed_files(
25+
single_commit="64950a634cd11d1a01ddfedaeffed67b531cb11e"
26+
)
27+
28+
# Then
29+
mock_call.assert_called_with(
30+
["git", "diff", "64950a634cd11d1a01ddfedaeffed67b531cb11e^!", "--name-only"],
31+
cwd="/bin",
32+
stderr=STDOUT,
33+
)
34+
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
35+
36+
37+
@patch("subprocess.check_output")
38+
def test_changed_files_working_tree(mock_call):
39+
# Given
40+
mock_call.return_value = b"file1.ipynb\nfile2.py"
41+
42+
# When
43+
actual_response = Git(root_dir="/bin").changed_files(base="WORKING", remote="HEAD")
44+
45+
# Then
46+
mock_call.assert_called_with(
47+
["git", "diff", "HEAD", "--name-only"], cwd="/bin", stderr=STDOUT
48+
)
49+
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
50+
51+
52+
@patch("subprocess.check_output")
53+
def test_changed_files_index(mock_call):
54+
# Given
55+
mock_call.return_value = b"file1.ipynb\nfile2.py"
56+
57+
# When
58+
actual_response = Git(root_dir="/bin").changed_files(base="INDEX", remote="HEAD")
59+
60+
# Then
61+
mock_call.assert_called_with(
62+
["git", "diff", "--staged", "HEAD", "--name-only"], cwd="/bin", stderr=STDOUT
63+
)
64+
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
65+
66+
67+
@patch("subprocess.check_output")
68+
def test_changed_files_two_commits(mock_call):
69+
# Given
70+
mock_call.return_value = b"file1.ipynb\nfile2.py"
71+
72+
# When
73+
actual_response = Git(root_dir="/bin").changed_files(
74+
base="HEAD", remote="origin/HEAD"
75+
)
76+
77+
# Then
78+
mock_call.assert_called_with(
79+
["git", "diff", "HEAD", "origin/HEAD", "--name-only"], cwd="/bin", stderr=STDOUT
80+
)
81+
assert {"code": 0, "files": ["file1.ipynb", "file2.py"]} == actual_response
82+
83+
84+
@patch("subprocess.check_output")
85+
def test_changed_files_git_diff_error(mock_call):
86+
# Given
87+
mock_call.side_effect = CalledProcessError(128, "cmd", b"error message")
88+
89+
# When
90+
actual_response = Git(root_dir="/bin").changed_files(
91+
base="HEAD", remote="origin/HEAD"
92+
)
93+
94+
# Then
95+
mock_call.assert_called_with(
96+
["git", "diff", "HEAD", "origin/HEAD", "--name-only"], cwd="/bin", stderr=STDOUT
97+
)
98+
assert {"code": 128, "message": "error message"} == actual_response

0 commit comments

Comments
 (0)