Skip to content
This repository was archived by the owner on Sep 27, 2019. It is now read-only.

Commit 9e5be9b

Browse files
authored
Merge pull request #1329 from tcm-marcel/feature/clangformat_hunks
Run clang-format on staged hunks only instead of whole file
2 parents 89ddd0b + 22aa648 commit 9e5be9b

File tree

2 files changed

+123
-33
lines changed

2 files changed

+123
-33
lines changed

script/formatting/formatter.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
os.path.abspath(os.path.dirname(__file__)).replace('/formatting', '')
2121
)
2222
from helpers import CLANG_FORMAT, PELOTON_DIR, CLANG_FORMAT_FILE, LOG,\
23-
clang_format
23+
clang_format, hunks_from_staged_files, hunks_from_last_commits
2424

2525
## ==============================================
2626
## CONFIGURATION
@@ -69,7 +69,7 @@
6969
## ==============================================
7070

7171

72-
def format_file(file_path, update_header, clang_format_code):
72+
def format_file(file_path, file_hunks, update_header, clang_format_code):
7373
"""Formats the file passed as argument."""
7474
file_name = os.path.basename(file_path)
7575
abs_path = os.path.abspath(file_path)
@@ -104,7 +104,7 @@ def format_file(file_path, update_header, clang_format_code):
104104
file.write(file_data)
105105

106106
elif clang_format_code:
107-
clang_format(file_path)
107+
clang_format(file_path, file_hunks)
108108

109109
#END WITH
110110
#END FORMAT__FILE(FILE_NAME)
@@ -118,7 +118,7 @@ def format_dir(dir_path, update_header, clang_format_code):
118118
file_path = subdir + os.path.sep + file
119119

120120
if file_path.endswith(".h") or file_path.endswith(".cpp"):
121-
format_file(file_path, update_header, clang_format_code)
121+
format_file(file_path, None, update_header, clang_format_code)
122122
#END IF
123123
#END FOR [file]
124124
#END FOR [os.walk]
@@ -147,43 +147,58 @@ def format_dir(dir_path, update_header, clang_format_code):
147147
)
148148
PARSER.add_argument(
149149
"-f", "--staged-files",
150-
help='Action: Apply the selected action(s) to all staged files (git)',
150+
help='Action: Apply the selected action(s) to all staged files (git). ' +
151+
'(clang-format will only touch the staged lines)',
151152
action='store_true'
152153
)
154+
PARSER.add_argument(
155+
"-n", "--number-commits",
156+
help='Action: Apply the selected action(s) to all changes of the last ' +
157+
'<n> commits (clang-format will only touch the changed lines)',
158+
type=int, default=0
159+
)
153160
PARSER.add_argument(
154161
'paths', metavar='PATH', type=str, nargs='*',
155162
help='Files or directories to (recursively) apply the actions to'
156163
)
157164

158165
ARGS = PARSER.parse_args()
159166

167+
# TARGETS is a list of files with an optional list of hunks, represented as
168+
# pair (start, end) of line numbers, 1 based.
169+
# element of TARGETS: (filename, None) or (filename, [(start,end)])
170+
160171
if ARGS.staged_files:
161-
PELOTON_DIR_bytes = bytes(PELOTON_DIR, 'utf-8')
162-
TARGETS = [
163-
str(os.path.abspath(os.path.join(PELOTON_DIR_bytes, f)), 'utf-8') \
164-
for f in subprocess.check_output(
165-
["git", "diff", "--name-only", "HEAD", "--cached",
166-
"--diff-filter=d"
167-
]
168-
).split()]
172+
TARGETS = hunks_from_staged_files()
169173

170174
if not TARGETS:
171175
LOG.error(
172176
"no staged files or not calling from a repository -- exiting"
173177
)
174178
sys.exit("no staged files or not calling from a repository")
179+
180+
elif ARGS.number_commits > 0:
181+
TARGETS = hunks_from_last_commits(ARGS.number_commits)
182+
183+
if not TARGETS:
184+
LOG.error(
185+
"no changes could be extracted for formatting -- exiting"
186+
)
187+
sys.exit("no changes could be extracted for formatting")
188+
175189
elif not ARGS.paths:
176190
LOG.error("no files or directories given -- exiting")
177191
sys.exit("no files or directories given")
192+
178193
else:
179-
TARGETS = ARGS.paths
180-
181-
for x in TARGETS:
182-
if os.path.isfile(x):
183-
LOG.info("Scanning file: %s", x)
184-
format_file(x, ARGS.update_header, ARGS.clang_format_code)
185-
elif os.path.isdir(x):
186-
LOG.info("Scanning directory %s", x)
187-
format_dir(x, ARGS.update_header, ARGS.clang_format_code)
194+
TARGETS = [(f, None) for f in ARGS.paths]
195+
196+
for f, hunks in TARGETS:
197+
if os.path.isfile(f):
198+
LOG.info("Scanning file: %s", f)
199+
format_file(f, hunks, ARGS.update_header, ARGS.clang_format_code)
200+
elif os.path.isdir(f):
201+
LOG.info("Scanning directory %s", f)
202+
format_dir(f, ARGS.update_header, ARGS.clang_format_code)
188203
## FOR
189204
## IF

script/helpers.py

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import os
77
import subprocess
8+
import re
89

910
from functools import reduce
1011

@@ -16,6 +17,9 @@
1617
# Fill me
1718
]
1819

20+
DIFF_FILE_PATT = re.compile(r'^\+\+\+ b\/(.*)')
21+
DIFF_HUNK_PATT = re.compile(r'^@@ \-\d+(,\d+)? \+(\d+)(,)?(\d+)? @@.*')
22+
1923
## ==============================================
2024
## LOGGING CONFIGURATION
2125
## ==============================================
@@ -44,21 +48,20 @@ def find_clangformat():
4448
CLANG_FORMAT = find_clangformat()
4549
CLANG_COMMAND_PREFIX = [CLANG_FORMAT, "-style=file"]
4650

47-
def clang_check(file_path):
51+
def clang_check(file_path, hunks=None):
4852
"""Checks and reports bad code formatting."""
53+
54+
assert not file_path is None and not file_path == ""
55+
4956
rel_path_from_peloton_dir = os.path.relpath(file_path, PELOTON_DIR)
5057

5158
if rel_path_from_peloton_dir in FORMATTING_FILE_WHITELIST:
5259
return True
5360

5461
file_status = True
5562

56-
# Run clang-format on the file
57-
if CLANG_FORMAT is None:
58-
LOG.error("clang-format seems not installed")
59-
exit()
60-
clang_format_cmd = CLANG_COMMAND_PREFIX + [file_path]
61-
formatted_src = subprocess.check_output(clang_format_cmd).splitlines(True)
63+
# Run clang-format on the file and get output (not inline!)
64+
formatted_src = clang_format(file_path, None, inline=False)
6265

6366
# For Python 3, the above command gives a list of binary sequences, each
6467
# of which has to be converted to string for diff to operate correctly.
@@ -86,12 +89,84 @@ def clang_check(file_path):
8689
return file_status
8790

8891

89-
def clang_format(file_path):
90-
"""Formats the file at file_path"""
92+
def clang_format(file_path, hunks=None, inline=True):
93+
"""Formats the file at file_path.
94+
'hunks' can be a list of pairs with (start,end) line numbers, 1 based.
95+
"""
96+
97+
assert not file_path is None and not file_path == ""
98+
9199
if CLANG_FORMAT is None:
92100
LOG.error("clang-format seems not installed")
93101
exit()
94102

95-
formatting_command = CLANG_COMMAND_PREFIX + ["-i", file_path]
103+
formatting_command = CLANG_COMMAND_PREFIX + [file_path]
104+
105+
if inline:
106+
formatting_command.append("-i")
107+
108+
if not hunks is None:
109+
for start, end in hunks:
110+
if start > 0 and end > 0:
111+
formatting_command.append("-lines={}:{}".format(start, end))
112+
96113
LOG.info(' '.join(formatting_command))
97-
subprocess.call(formatting_command)
114+
output = subprocess.check_output(formatting_command).splitlines(True)
115+
return output
116+
117+
118+
def hunks_from_last_commits(n):
119+
""" Extract hunks of the last n commits. """
120+
121+
assert n > 0
122+
123+
diff_output = subprocess.check_output(["git", "diff", "HEAD~"+str(n) , "--diff-filter=d", "--unified=0"]
124+
).decode("utf-8").splitlines()
125+
126+
return _hunks_from_diff(diff_output)
127+
128+
129+
def hunks_from_staged_files():
130+
diff_output = subprocess.check_output(["git", "diff", "HEAD",
131+
"--cached", "--diff-filter=d", "--unified=0"]
132+
).decode("utf-8").splitlines()
133+
134+
return _hunks_from_diff(diff_output)
135+
136+
137+
def _hunks_from_diff(diff_output):
138+
""" Parse a diff output and extract the hunks of changed files.
139+
The diff output must not have additional lines!
140+
(use --unified=0) """
141+
142+
# TARGETS is a list of files with an optional list of hunks, represented as
143+
# pair (start, end) of line numbers, 1 based.
144+
# element of TARGETS: (filename, None) or (filename, [(start,end)])
145+
target_files = []
146+
147+
# hunks_current_list serves as a reference to the hunks list of the
148+
# last added file
149+
hunks_current_list = None
150+
151+
for line in diff_output:
152+
file_match = DIFF_FILE_PATT.search(line)
153+
hunk_match = DIFF_HUNK_PATT.search(line)
154+
if file_match:
155+
file_path = os.path.abspath(os.path.join(PELOTON_DIR,
156+
file_match.group(1)))
157+
158+
hunks_current_list = []
159+
if file_path.endswith(".h") or file_path.endswith(".cpp"):
160+
target_files.append((file_path, hunks_current_list))
161+
# If this file is not .cpp/.h the hunks_current_list reference
162+
# will point to an empty list which will be discarded later
163+
elif hunk_match:
164+
# add entry in the hunk list of the last file
165+
if hunk_match.group(4) is None:
166+
hunk = (int(hunk_match.group(2)), int(hunk_match.group(2)))
167+
else:
168+
hunk = (int(hunk_match.group(2)), int(hunk_match.group(2)) +
169+
int(hunk_match.group(4)))
170+
hunks_current_list.append(hunk)
171+
172+
return target_files

0 commit comments

Comments
 (0)