Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
B
bcc
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
bcc
Commits
b3abbfc0
Commit
b3abbfc0
authored
Apr 19, 2016
by
Brenden Blanco
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add git-clang-format to be used for testing source style
parent
49d2ca8c
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
485 additions
and
0 deletions
+485
-0
scripts/git-clang-format
scripts/git-clang-format
+485
-0
No files found.
scripts/git-clang-format
0 → 100755
View file @
b3abbfc0
#!/usr/bin/env python
#
#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===#
#
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
#===------------------------------------------------------------------------===#
r"""
clang-format git integration
============================
This file provides a clang-format integration for git. Put it somewhere in your
path and ensure that it is executable. Then, "git clang-format" will invoke
clang-format on the changes in current files or a specific commit.
For further details, run:
git clang-format -h
Requires Python 2.7
"""
import
argparse
import
collections
import
contextlib
import
errno
import
os
import
re
import
subprocess
import
sys
usage
=
'git clang-format [OPTIONS] [<commit>] [--] [<file>...]'
desc
=
'''
Run clang-format on all lines that differ between the working directory
and <commit>, which defaults to HEAD. Changes are only applied to the working
directory.
The following git-config settings set the default of the corresponding option:
clangFormat.binary
clangFormat.commit
clangFormat.extension
clangFormat.style
'''
# Name of the temporary index file in which save the output of clang-format.
# This file is created within the .git directory.
temp_index_basename
=
'clang-format-index'
Range
=
collections
.
namedtuple
(
'Range'
,
'start, count'
)
def
main
():
config
=
load_git_config
()
# In order to keep '--' yet allow options after positionals, we need to
# check for '--' ourselves. (Setting nargs='*' throws away the '--', while
# nargs=argparse.REMAINDER disallows options after positionals.)
argv
=
sys
.
argv
[
1
:]
try
:
idx
=
argv
.
index
(
'--'
)
except
ValueError
:
dash_dash
=
[]
else
:
dash_dash
=
argv
[
idx
:]
argv
=
argv
[:
idx
]
default_extensions
=
','
.
join
([
# From clang/lib/Frontend/FrontendOptions.cpp, all lower case
'c'
,
'h'
,
# C
'm'
,
# ObjC
'mm'
,
# ObjC++
'cc'
,
'cp'
,
'cpp'
,
'c++'
,
'cxx'
,
'hpp'
,
# C++
# Other languages that clang-format supports
'proto'
,
'protodevel'
,
# Protocol Buffers
'js'
,
# JavaScript
'ts'
,
# TypeScript
])
p
=
argparse
.
ArgumentParser
(
usage
=
usage
,
formatter_class
=
argparse
.
RawDescriptionHelpFormatter
,
description
=
desc
)
p
.
add_argument
(
'--binary'
,
default
=
config
.
get
(
'clangformat.binary'
,
'clang-format'
),
help
=
'path to clang-format'
),
p
.
add_argument
(
'--commit'
,
default
=
config
.
get
(
'clangformat.commit'
,
'HEAD'
),
help
=
'default commit to use if none is specified'
),
p
.
add_argument
(
'--diff'
,
action
=
'store_true'
,
help
=
'print a diff instead of applying the changes'
)
p
.
add_argument
(
'--extensions'
,
default
=
config
.
get
(
'clangformat.extensions'
,
default_extensions
),
help
=
(
'comma-separated list of file extensions to format, '
'excluding the period and case-insensitive'
)),
p
.
add_argument
(
'-f'
,
'--force'
,
action
=
'store_true'
,
help
=
'allow changes to unstaged files'
)
p
.
add_argument
(
'-p'
,
'--patch'
,
action
=
'store_true'
,
help
=
'select hunks interactively'
)
p
.
add_argument
(
'-q'
,
'--quiet'
,
action
=
'count'
,
default
=
0
,
help
=
'print less information'
)
p
.
add_argument
(
'--style'
,
default
=
config
.
get
(
'clangformat.style'
,
None
),
help
=
'passed to clang-format'
),
p
.
add_argument
(
'-v'
,
'--verbose'
,
action
=
'count'
,
default
=
0
,
help
=
'print extra information'
)
# We gather all the remaining positional arguments into 'args' since we need
# to use some heuristics to determine whether or not <commit> was present.
# However, to print pretty messages, we make use of metavar and help.
p
.
add_argument
(
'args'
,
nargs
=
'*'
,
metavar
=
'<commit>'
,
help
=
'revision from which to compute the diff'
)
p
.
add_argument
(
'ignored'
,
nargs
=
'*'
,
metavar
=
'<file>...'
,
help
=
'if specified, only consider differences in these files'
)
opts
=
p
.
parse_args
(
argv
)
opts
.
verbose
-=
opts
.
quiet
del
opts
.
quiet
commit
,
files
=
interpret_args
(
opts
.
args
,
dash_dash
,
opts
.
commit
)
changed_lines
=
compute_diff_and_extract_lines
(
commit
,
files
)
if
opts
.
verbose
>=
1
:
ignored_files
=
set
(
changed_lines
)
filter_by_extension
(
changed_lines
,
opts
.
extensions
.
lower
().
split
(
','
))
if
opts
.
verbose
>=
1
:
ignored_files
.
difference_update
(
changed_lines
)
if
ignored_files
:
print
'Ignoring changes in the following files (wrong extension):'
for
filename
in
ignored_files
:
print
' '
,
filename
if
changed_lines
:
print
'Running clang-format on the following files:'
for
filename
in
changed_lines
:
print
' '
,
filename
if
not
changed_lines
:
print
'no modified files to format'
return
# The computed diff outputs absolute paths, so we must cd before accessing
# those files.
cd_to_toplevel
()
old_tree
=
create_tree_from_workdir
(
changed_lines
)
new_tree
=
run_clang_format_and_save_to_tree
(
changed_lines
,
binary
=
opts
.
binary
,
style
=
opts
.
style
)
if
opts
.
verbose
>=
1
:
print
'old tree:'
,
old_tree
print
'new tree:'
,
new_tree
if
old_tree
==
new_tree
:
if
opts
.
verbose
>=
0
:
print
'clang-format did not modify any files'
elif
opts
.
diff
:
print_diff
(
old_tree
,
new_tree
)
else
:
changed_files
=
apply_changes
(
old_tree
,
new_tree
,
force
=
opts
.
force
,
patch_mode
=
opts
.
patch
)
if
(
opts
.
verbose
>=
0
and
not
opts
.
patch
)
or
opts
.
verbose
>=
1
:
print
'changed files:'
for
filename
in
changed_files
:
print
' '
,
filename
def
load_git_config
(
non_string_options
=
None
):
"""Return the git configuration as a dictionary.
All options are assumed to be strings unless in `non_string_options`, in which
is a dictionary mapping option name (in lower case) to either "--bool" or
"--int"."""
if
non_string_options
is
None
:
non_string_options
=
{}
out
=
{}
for
entry
in
run
(
'git'
,
'config'
,
'--list'
,
'--null'
).
split
(
'
\
0
'
):
if
entry
:
name
,
value
=
entry
.
split
(
'
\
n
'
,
1
)
if
name
in
non_string_options
:
value
=
run
(
'git'
,
'config'
,
non_string_options
[
name
],
name
)
out
[
name
]
=
value
return
out
def
interpret_args
(
args
,
dash_dash
,
default_commit
):
"""Interpret `args` as "[commit] [--] [files...]" and return (commit, files).
It is assumed that "--" and everything that follows has been removed from
args and placed in `dash_dash`.
If "--" is present (i.e., `dash_dash` is non-empty), the argument to its
left (if present) is taken as commit. Otherwise, the first argument is
checked if it is a commit or a file. If commit is not given,
`default_commit` is used."""
if
dash_dash
:
if
len
(
args
)
==
0
:
commit
=
default_commit
elif
len
(
args
)
>
1
:
die
(
'at most one commit allowed; %d given'
%
len
(
args
))
else
:
commit
=
args
[
0
]
object_type
=
get_object_type
(
commit
)
if
object_type
not
in
(
'commit'
,
'tag'
):
if
object_type
is
None
:
die
(
"'%s' is not a commit"
%
commit
)
else
:
die
(
"'%s' is a %s, but a commit was expected"
%
(
commit
,
object_type
))
files
=
dash_dash
[
1
:]
elif
args
:
if
disambiguate_revision
(
args
[
0
]):
commit
=
args
[
0
]
files
=
args
[
1
:]
else
:
commit
=
default_commit
files
=
args
else
:
commit
=
default_commit
files
=
[]
return
commit
,
files
def
disambiguate_revision
(
value
):
"""Returns True if `value` is a revision, False if it is a file, or dies."""
# If `value` is ambiguous (neither a commit nor a file), the following
# command will die with an appropriate error message.
run
(
'git'
,
'rev-parse'
,
value
,
verbose
=
False
)
object_type
=
get_object_type
(
value
)
if
object_type
is
None
:
return
False
if
object_type
in
(
'commit'
,
'tag'
):
return
True
die
(
'`%s` is a %s, but a commit or filename was expected'
%
(
value
,
object_type
))
def
get_object_type
(
value
):
"""Returns a string description of an object's type, or None if it is not
a valid git object."""
cmd
=
[
'git'
,
'cat-file'
,
'-t'
,
value
]
p
=
subprocess
.
Popen
(
cmd
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
)
stdout
,
stderr
=
p
.
communicate
()
if
p
.
returncode
!=
0
:
return
None
return
stdout
.
strip
()
def
compute_diff_and_extract_lines
(
commit
,
files
):
"""Calls compute_diff() followed by extract_lines()."""
diff_process
=
compute_diff
(
commit
,
files
)
changed_lines
=
extract_lines
(
diff_process
.
stdout
)
diff_process
.
stdout
.
close
()
diff_process
.
wait
()
if
diff_process
.
returncode
!=
0
:
# Assume error was already printed to stderr.
sys
.
exit
(
2
)
return
changed_lines
def
compute_diff
(
commit
,
files
):
"""Return a subprocess object producing the diff from `commit`.
The return value's `stdin` file object will produce a patch with the
differences between the working directory and `commit`, filtered on `files`
(if non-empty). Zero context lines are used in the patch."""
cmd
=
[
'git'
,
'diff-index'
,
'-p'
,
'-U0'
,
commit
,
'--'
]
cmd
.
extend
(
files
)
p
=
subprocess
.
Popen
(
cmd
,
stdin
=
subprocess
.
PIPE
,
stdout
=
subprocess
.
PIPE
)
p
.
stdin
.
close
()
return
p
def
extract_lines
(
patch_file
):
"""Extract the changed lines in `patch_file`.
The return value is a dictionary mapping filename to a list of (start_line,
line_count) pairs.
The input must have been produced with ``-U0``, meaning unidiff format with
zero lines of context. The return value is a dict mapping filename to a
list of line `Range`s."""
matches
=
{}
for
line
in
patch_file
:
match
=
re
.
search
(
r'^\
+
\+\
+
\ [^/]+/(.*)'
,
line
)
if
match
:
filename
=
match
.
group
(
1
).
rstrip
(
'
\
r
\
n
'
)
match
=
re
.
search
(
r'^@@ -[0-9,]+ \
+(
\d+)(,(\
d+))?
', line)
if match:
start_line = int(match.group(1))
line_count = 1
if match.group(3):
line_count = int(match.group(3))
if line_count > 0:
matches.setdefault(filename, []).append(Range(start_line, line_count))
return matches
def filter_by_extension(dictionary, allowed_extensions):
"""Delete every key in `dictionary` that doesn'
t
have
an
allowed
extension
.
`allowed_extensions`
must
be
a
collection
of
lowercase
file
extensions
,
excluding
the
period
.
"""
allowed_extensions = frozenset(allowed_extensions)
for filename in dictionary.keys():
base_ext = filename.rsplit('.', 1)
if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
del dictionary[filename]
def cd_to_toplevel():
"""
Change
to
the
top
level
of
the
git
repository
.
"""
toplevel = run('git', 'rev-parse', '--show-toplevel')
os.chdir(toplevel)
def create_tree_from_workdir(filenames):
"""
Create
a
new
git
tree
with
the
given
files
from
the
working
directory
.
Returns
the
object
ID
(
SHA
-
1
)
of
the
created
tree
.
"""
return create_tree(filenames, '--stdin')
def run_clang_format_and_save_to_tree(changed_lines, binary='clang-format',
style=None):
"""
Run
clang
-
format
on
each
file
and
save
the
result
to
a
git
tree
.
Returns
the
object
ID
(
SHA
-
1
)
of
the
created
tree
.
"""
def index_info_generator():
for filename, line_ranges in changed_lines.iteritems():
mode = oct(os.stat(filename).st_mode)
blob_id = clang_format_to_blob(filename, line_ranges, binary=binary,
style=style)
yield '%s %s
\
t
%s' % (mode, blob_id, filename)
return create_tree(index_info_generator(), '--index-info')
def create_tree(input_lines, mode):
"""
Create
a
tree
object
from
the
given
input
.
If
mode
is
'--stdin'
,
it
must
be
a
list
of
filenames
.
If
mode
is
'--index-info'
is
must
be
a
list
of
values
suitable
for
"git update-index
--index-info"
,
such
as
"<mode> <SP> <sha1> <TAB> <filename>"
.
Any
other
mode
is
invalid
.
"""
assert mode in ('--stdin', '--index-info')
cmd = ['git', 'update-index', '--add', '-z', mode]
with temporary_index_file():
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for line in input_lines:
p.stdin.write('%s
\
0
' % line)
p.stdin.close()
if p.wait() != 0:
die('`%s` failed' % ' '.join(cmd))
tree_id = run('git', 'write-tree')
return tree_id
def clang_format_to_blob(filename, line_ranges, binary='clang-format',
style=None):
"""
Run
clang
-
format
on
the
given
file
and
save
the
result
to
a
git
blob
.
Returns
the
object
ID
(
SHA
-
1
)
of
the
created
blob
.
"""
clang_format_cmd = [binary, filename]
if style:
clang_format_cmd.extend(['-style='+style])
clang_format_cmd.extend([
'-lines=%s:%s' % (start_line, start_line+line_count-1)
for start_line, line_count in line_ranges])
try:
clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
except OSError as e:
if e.errno == errno.ENOENT:
die('cannot find executable "%s"' % binary)
else:
raise
clang_format.stdin.close()
hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
stdout=subprocess.PIPE)
clang_format.stdout.close()
stdout = hash_object.communicate()[0]
if hash_object.returncode != 0:
die('`%s` failed' % ' '.join(hash_object_cmd))
if clang_format.wait() != 0:
die('`%s` failed' % ' '.join(clang_format_cmd))
return stdout.rstrip('
\
r
\
n
')
@contextlib.contextmanager
def temporary_index_file(tree=None):
"""
Context
manager
for
setting
GIT_INDEX_FILE
to
a
temporary
file
and
deleting
the
file
afterward
.
"""
index_path = create_temporary_index(tree)
old_index_path = os.environ.get('GIT_INDEX_FILE')
os.environ['GIT_INDEX_FILE'] = index_path
try:
yield
finally:
if old_index_path is None:
del os.environ['GIT_INDEX_FILE']
else:
os.environ['GIT_INDEX_FILE'] = old_index_path
os.remove(index_path)
def create_temporary_index(tree=None):
"""
Create
a
temporary
index
file
and
return
the
created
file
's path.
If `tree` is not None, use that as the tree to read in. Otherwise, an
empty index is created."""
gitdir = run('
git
', '
rev
-
parse
', '
--
git
-
dir
')
path = os.path.join(gitdir, temp_index_basename)
if tree is None:
tree = '
--
empty
'
run('
git
', '
read
-
tree
', '
--
index
-
output
=
'+path, tree)
return path
def print_diff(old_tree, new_tree):
"""Print the diff between the two trees to stdout."""
# We use the porcelain '
diff
' and not plumbing '
diff
-
tree
' because the output
# is expected to be viewed by the user, and only the former does nice things
# like color and pagination.
subprocess.check_call(['
git
', '
diff
', old_tree, new_tree, '
--
'])
def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
"""Apply the changes in `new_tree` to the working directory.
Bails if there are local changes in those files and not `force`. If
`patch_mode`, runs `git checkout --patch` to select hunks interactively."""
changed_files = run('
git
', '
diff
-
tree
', '
-
r', '
-
z
', '
--
name
-
only
', old_tree,
new_tree).rstrip('
\
0
').split('
\
0
')
if not force:
unstaged_files = run('
git
', '
diff
-
files
', '
--
name
-
status
', *changed_files)
if unstaged_files:
print >>sys.stderr, ('
The
following
files
would
be
modified
but
'
'
have
unstaged
changes
:
')
print >>sys.stderr, unstaged_files
print >>sys.stderr, '
Please
commit
,
stage
,
or
stash
them
first
.
'
sys.exit(2)
if patch_mode:
# In patch mode, we could just as well create an index from the new tree
# and checkout from that, but then the user will be presented with a
# message saying "Discard ... from worktree". Instead, we use the old
# tree as the index and checkout from new_tree, which gives the slightly
# better message, "Apply ... to index and worktree". This is not quite
# right, since it won'
t
be
applied
to
the
user
's index, but oh well.
with temporary_index_file(old_tree):
subprocess.check_call(['
git
', '
checkout
', '
--
patch
', new_tree])
index_tree = old_tree
else:
with temporary_index_file(new_tree):
run('
git
', '
checkout
-
index
', '
-
a
', '
-
f')
return changed_files
def run(*args, **kwargs):
stdin = kwargs.pop('
stdin
', '')
verbose = kwargs.pop('
verbose
', True)
strip = kwargs.pop('
strip
', True)
for name in kwargs:
raise TypeError("run() got an unexpected keyword argument '
%
s
'" % name)
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
stdout, stderr = p.communicate(input=stdin)
if p.returncode == 0:
if stderr:
if verbose:
print >>sys.stderr, '
`%s`
printed
to
stderr
:
' % '
'.join(args)
print >>sys.stderr, stderr.rstrip()
if strip:
stdout = stdout.rstrip('
\
r
\
n
')
return stdout
if verbose:
print >>sys.stderr, '
`%s`
returned
%
s
' % ('
'.join(args), p.returncode)
if stderr:
print >>sys.stderr, stderr.rstrip()
sys.exit(2)
def die(message):
print >>sys.stderr, '
error
:
', message
sys.exit(2)
if __name__ == '
__main__
':
main()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment