@@ -101,6 +101,12 @@ def call_subprocess(
101
101
102
102
return code , output , error
103
103
104
+ def strip_and_split (s ):
105
+ """strip trailing \x00 and split on \x00
106
+
107
+ Useful for parsing output of git commands with -z flag.
108
+ """
109
+ return s .strip ("\x00 " ).split ("\x00 " )
104
110
105
111
class Git :
106
112
"""
@@ -113,7 +119,7 @@ def __init__(self, contents_manager):
113
119
114
120
async def config (self , top_repo_path , ** kwargs ):
115
121
"""Get or set Git options.
116
-
122
+
117
123
If no kwargs, all options are returned. Otherwise kwargs are set.
118
124
"""
119
125
response = {"code" : 1 }
@@ -154,12 +160,12 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
154
160
There are two reserved "refs" for the base
155
161
1. WORKING : Represents the Git working tree
156
162
2. INDEX: Represents the Git staging area / index
157
-
163
+
158
164
Keyword Arguments:
159
165
single_commit {string} -- The single commit ref
160
166
base {string} -- the base Git ref
161
167
remote {string} -- the remote Git ref
162
-
168
+
163
169
Returns:
164
170
dict -- the response of format {
165
171
"code": int, # Command status code
@@ -168,14 +174,14 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
168
174
}
169
175
"""
170
176
if single_commit :
171
- cmd = ["git" , "diff" , "{}^!" .format (single_commit ), "--name-only" ]
177
+ cmd = ["git" , "diff" , "{}^!" .format (single_commit ), "--name-only" , "-z" ]
172
178
elif base and remote :
173
179
if base == "WORKING" :
174
- cmd = ["git" , "diff" , remote , "--name-only" ]
180
+ cmd = ["git" , "diff" , remote , "--name-only" , "-z" ]
175
181
elif base == "INDEX" :
176
- cmd = ["git" , "diff" , "--staged" , remote , "--name-only" ]
182
+ cmd = ["git" , "diff" , "--staged" , remote , "--name-only" , "-z" ]
177
183
else :
178
- cmd = ["git" , "diff" , base , remote , "--name-only" ]
184
+ cmd = ["git" , "diff" , base , remote , "--name-only" , "-z" ]
179
185
else :
180
186
raise tornado .web .HTTPError (
181
187
400 , "Either single_commit or (base and remote) must be provided"
@@ -193,7 +199,7 @@ async def changed_files(self, base=None, remote=None, single_commit=None):
193
199
response ["command" ] = " " .join (cmd )
194
200
response ["message" ] = error
195
201
else :
196
- response ["files" ] = output . strip (). split ( " \n " )
202
+ response ["files" ] = strip_and_split ( output )
197
203
198
204
return response
199
205
@@ -236,7 +242,7 @@ async def status(self, current_path):
236
242
"""
237
243
Execute git status command & return the result.
238
244
"""
239
- cmd = ["git" , "status" , "--porcelain" , "-u" ]
245
+ cmd = ["git" , "status" , "--porcelain" , "-u" , "-z" ]
240
246
code , my_output , my_error = await execute (
241
247
cmd , cwd = os .path .join (self .root_dir , current_path ),
242
248
)
@@ -249,20 +255,15 @@ async def status(self, current_path):
249
255
}
250
256
251
257
result = []
252
- line_array = my_output .splitlines ()
253
- for line in line_array :
254
- to1 = None
255
- from_path = line [3 :]
256
- if line [0 ] == "R" :
257
- to0 = line [3 :].split (" -> " )
258
- to1 = to0 [len (to0 ) - 1 ]
259
- else :
260
- to1 = line [3 :]
261
- if to1 .startswith ('"' ):
262
- to1 = to1 [1 :]
263
- if to1 .endswith ('"' ):
264
- to1 = to1 [:- 1 ]
265
- result .append ({"x" : line [0 ], "y" : line [1 ], "to" : to1 , "from" : from_path })
258
+ line_iterable = iter (strip_and_split (my_output ))
259
+ for line in line_iterable :
260
+ result .append ({
261
+ "x" : line [0 ],
262
+ "y" : line [1 ],
263
+ "to" : line [3 :],
264
+ # if file was renamed, next line contains original path
265
+ "from" : next (line_iterable ) if line [0 ]== 'R' else line [3 :]
266
+ })
266
267
return {"code" : code , "files" : result }
267
268
268
269
async def log (self , current_path , history_count = 10 ):
@@ -311,73 +312,69 @@ async def log(self, current_path, history_count=10):
311
312
312
313
async def detailed_log (self , selected_hash , current_path ):
313
314
"""
314
- Execute git log -1 --stat -- numstat --oneline command (used to get
315
+ Execute git log -1 --numstat --oneline -z command (used to get
315
316
insertions & deletions per file) & return the result.
316
317
"""
317
- cmd = ["git" , "log" , "-1" , "--stat " , "--numstat " , "--oneline " , selected_hash ]
318
+ cmd = ["git" , "log" , "-1" , "--numstat " , "--oneline " , "-z " , selected_hash ]
318
319
code , my_output , my_error = await execute (
319
320
cmd , cwd = os .path .join (self .root_dir , current_path ),
320
321
)
321
322
322
323
if code != 0 :
323
324
return {"code" : code , "command" : " " .join (cmd ), "message" : my_error }
324
325
326
+ total_insertions = 0
327
+ total_deletions = 0
325
328
result = []
326
- note = [0 ] * 3
327
- count = 0
328
- temp = ""
329
- line_array = my_output .splitlines ()
330
- length = len (line_array )
331
- INSERTION_INDEX = 0
332
- DELETION_INDEX = 1
333
- MODIFIED_FILE_PATH_INDEX = 2
334
- if length > 1 :
335
- temp = line_array [length - 1 ]
336
- words = temp .split ()
337
- for i in range (0 , len (words )):
338
- if words [i ].isdigit ():
339
- note [count ] = words [i ]
340
- count += 1
341
- for num in range (1 , int (length / 2 )):
342
- line_info = line_array [num ].split (maxsplit = 2 )
343
- words = line_info [2 ].split ("/" )
344
- length = len (words )
345
- result .append (
346
- {
347
- "modified_file_path" : line_info [MODIFIED_FILE_PATH_INDEX ],
348
- "modified_file_name" : words [length - 1 ],
349
- "insertion" : line_info [INSERTION_INDEX ],
350
- "deletion" : line_info [DELETION_INDEX ],
351
- }
352
- )
353
-
354
- if note [2 ] == 0 and length > 1 :
355
- if "-" in temp :
356
- exchange = note [1 ]
357
- note [1 ] = note [2 ]
358
- note [2 ] = exchange
329
+ line_iterable = iter (strip_and_split (my_output )[1 :])
330
+ for line in line_iterable :
331
+ insertions , deletions , file = line .split ('\t ' )
332
+
333
+ if file == '' :
334
+ # file was renamed or moved, we need next two lines of output
335
+ from_path = next (line_iterable )
336
+ to_path = next (line_iterable )
337
+ modified_file_name = from_path + " => " + to_path
338
+ modified_file_path = to_path
339
+ else :
340
+ modified_file_name = file .split ("/" )[- 1 ]
341
+ modified_file_path = file
342
+
343
+ result .append ({
344
+ "modified_file_path" : modified_file_path ,
345
+ "modified_file_name" : modified_file_name ,
346
+ "insertion" : insertions ,
347
+ "deletion" : deletions ,
348
+ })
349
+ total_insertions += int (insertions )
350
+ total_deletions += int (deletions )
351
+
352
+ modified_file_note = "{num_files} files changed, {insertions} insertions(+), {deletions} deletions(-)" .format (
353
+ num_files = len (result ),
354
+ insertions = total_insertions ,
355
+ deletions = total_deletions )
359
356
360
357
return {
361
358
"code" : code ,
362
- "modified_file_note" : temp ,
363
- "modified_files_count" : note [ 0 ] ,
364
- "number_of_insertions" : note [ 1 ] ,
365
- "number_of_deletions" : note [ 2 ] ,
359
+ "modified_file_note" : modified_file_note ,
360
+ "modified_files_count" : str ( len ( result )) ,
361
+ "number_of_insertions" : str ( total_insertions ) ,
362
+ "number_of_deletions" : str ( total_deletions ) ,
366
363
"modified_files" : result ,
367
364
}
368
365
369
366
async def diff (self , top_repo_path ):
370
367
"""
371
368
Execute git diff command & return the result.
372
369
"""
373
- cmd = ["git" , "diff" , "--numstat" ]
370
+ cmd = ["git" , "diff" , "--numstat" , "-z" ]
374
371
code , my_output , my_error = await execute (cmd , cwd = top_repo_path )
375
372
376
373
if code != 0 :
377
374
return {"code" : code , "command" : " " .join (cmd ), "message" : my_error }
378
375
379
376
result = []
380
- line_array = my_output . splitlines ( )
377
+ line_array = strip_and_split ( my_output )
381
378
for line in line_array :
382
379
linesplit = line .split ()
383
380
result .append (
0 commit comments