1#!/usr/bin/env bash
2# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3# If clang_format_diff.py command is not specfied, we assume we are able to
4# access directly without any path.
5
6print_usage () {
7  echo "Usage:"
8  echo "format-diff.sh [OPTIONS]"
9  echo "-c: check only."
10  echo "-h: print this message."
11}
12
13while getopts ':ch' OPTION; do
14  case "$OPTION" in
15    c)
16      CHECK_ONLY=1
17      ;;
18    h)
19      print_usage
20      exit 1
21      ;;
22    ?)
23      print_usage
24      exit 1
25      ;;
26  esac
27done
28
29REPO_ROOT="$(git rev-parse --show-toplevel)"
30
31if [ "$CLANG_FORMAT_DIFF" ]; then
32  echo "Note: CLANG_FORMAT_DIFF='$CLANG_FORMAT_DIFF'"
33  # Dry run to confirm dependencies like argparse
34  if $CLANG_FORMAT_DIFF --help >/dev/null < /dev/null; then
35    true #Good
36  else
37    exit 128
38  fi
39else
40  # First try directly executing the possibilities
41  if clang-format-diff.py --help &> /dev/null < /dev/null; then
42    CLANG_FORMAT_DIFF=clang-format-diff.py
43  elif $REPO_ROOT/clang-format-diff.py --help &> /dev/null < /dev/null; then
44    CLANG_FORMAT_DIFF=$REPO_ROOT/clang-format-diff.py
45  else
46    # This probably means we need to directly invoke the interpreter.
47    # But first find clang-format-diff.py
48    if [ -f "$REPO_ROOT/clang-format-diff.py" ]; then
49      CFD_PATH="$REPO_ROOT/clang-format-diff.py"
50    elif which clang-format-diff.py &> /dev/null; then
51      CFD_PATH="$(which clang-format-diff.py)"
52    else
53      echo "You didn't have clang-format-diff.py and/or clang-format available in your computer!"
54      echo "You can download clang-format-diff.py by running: "
55      echo "    curl --location https://raw.githubusercontent.com/llvm/llvm-project/main/clang/tools/clang-format/clang-format-diff.py -o ${REPO_ROOT}/clang-format-diff.py"
56      echo "You should make sure the downloaded script is not compromised."
57      echo "You can download clang-format by running:"
58      echo "    brew install clang-format"
59      echo "  Or"
60      echo "    apt install clang-format"
61      echo "  This might work too:"
62      echo "    yum install git-clang-format"
63      echo "Then make sure clang-format is available and executable from \$PATH:"
64      echo "    clang-format --version"
65      exit 128
66    fi
67    # Check argparse pre-req on interpreter, or it will fail
68    if echo import argparse | ${PYTHON:-python3}; then
69      true # Good
70    else
71      echo "To run clang-format-diff.py, we'll need the library "argparse" to be"
72      echo "installed. You can try either of the follow ways to install it:"
73      echo "  1. Manually download argparse: https://pypi.python.org/pypi/argparse"
74      echo "  2. easy_install argparse (if you have easy_install)"
75      echo "  3. pip install argparse (if you have pip)"
76      exit 129
77    fi
78    # Unfortunately, some machines have a Python2 clang-format-diff.py
79    # installed but only a Python3 interpreter installed. Unfortunately,
80    # automatic 2to3 migration is insufficient, so suggest downloading latest.
81    if grep -q "print '" "$CFD_PATH" && \
82       ${PYTHON:-python3} --version | grep -q 'ython 3'; then
83      echo "You have clang-format-diff.py for Python 2 but are using a Python 3"
84      echo "interpreter (${PYTHON:-python3})."
85      echo "You can download clang-format-diff.py for Python 3 by running: "
86      echo "    curl --location https://raw.githubusercontent.com/llvm/llvm-project/main/clang/tools/clang-format/clang-format-diff.py -o ${REPO_ROOT}/clang-format-diff.py"
87      echo "You should make sure the downloaded script is not compromised."
88      exit 130
89    fi
90    CLANG_FORMAT_DIFF="${PYTHON:-python3} $CFD_PATH"
91    # This had better work after all those checks
92    if $CLANG_FORMAT_DIFF --help >/dev/null < /dev/null; then
93      true #Good
94    else
95      exit 128
96    fi
97  fi
98fi
99
100# TODO(kailiu) following work is not complete since we still need to figure
101# out how to add the modified files done pre-commit hook to git's commit index.
102#
103# Check if this script has already been added to pre-commit hook.
104# Will suggest user to add this script to pre-commit hook if their pre-commit
105# is empty.
106# PRE_COMMIT_SCRIPT_PATH="`git rev-parse --show-toplevel`/.git/hooks/pre-commit"
107# if ! ls $PRE_COMMIT_SCRIPT_PATH &> /dev/null
108# then
109#   echo "Would you like to add this script to pre-commit hook, which will do "
110#   echo -n "the format check for all the affected lines before you check in (y/n):"
111#   read add_to_hook
112#   if [ "$add_to_hook" == "y" ]
113#   then
114#     ln -s `git rev-parse --show-toplevel`/build_tools/format-diff.sh $PRE_COMMIT_SCRIPT_PATH
115#   fi
116# fi
117set -e
118
119uncommitted_code=`git diff HEAD`
120
121# If there's no uncommitted changes, we assume user are doing post-commit
122# format check, in which case we'll try to check the modified lines vs. the
123# facebook/rocksdb.git master branch. Otherwise, we'll check format of the
124# uncommitted code only.
125if [ -z "$uncommitted_code" ]
126then
127  # Attempt to get name of facebook/rocksdb.git remote.
128  [ "$FORMAT_REMOTE" ] || FORMAT_REMOTE="$(git remote -v | grep 'facebook/rocksdb.git' | head -n 1 | cut -f 1)"
129  # Fall back on 'origin' if that fails
130  [ "$FORMAT_REMOTE" ] || FORMAT_REMOTE=origin
131  # Use master branch from that remote
132  [ "$FORMAT_UPSTREAM" ] || FORMAT_UPSTREAM="$FORMAT_REMOTE/master"
133  # Get the common ancestor with that remote branch. Everything after that
134  # common ancestor would be considered the contents of a pull request, so
135  # should be relevant for formatting fixes.
136  FORMAT_UPSTREAM_MERGE_BASE="$(git merge-base "$FORMAT_UPSTREAM" HEAD)"
137  # Get the differences
138  diffs=$(git diff -U0 "$FORMAT_UPSTREAM_MERGE_BASE" | $CLANG_FORMAT_DIFF -p 1)
139  echo "Checking format of changes not yet in $FORMAT_UPSTREAM..."
140else
141  # Check the format of uncommitted lines,
142  diffs=$(git diff -U0 HEAD | $CLANG_FORMAT_DIFF -p 1)
143  echo "Checking format of uncommitted changes..."
144fi
145
146if [ -z "$diffs" ]
147then
148  echo "Nothing needs to be reformatted!"
149  exit 0
150elif [ $CHECK_ONLY ]
151then
152  echo "Your change has unformatted code. Please run make format!"
153  if [ $VERBOSE_CHECK ]; then
154    clang-format --version
155    echo "$diffs"
156  fi
157  exit 1
158fi
159
160# Highlight the insertion/deletion from the clang-format-diff.py's output
161COLOR_END="\033[0m"
162COLOR_RED="\033[0;31m"
163COLOR_GREEN="\033[0;32m"
164
165echo -e "Detect lines that doesn't follow the format rules:\r"
166# Add the color to the diff. lines added will be green; lines removed will be red.
167echo "$diffs" |
168  sed -e "s/\(^-.*$\)/`echo -e \"$COLOR_RED\1$COLOR_END\"`/" |
169  sed -e "s/\(^+.*$\)/`echo -e \"$COLOR_GREEN\1$COLOR_END\"`/"
170
171if [[ "$OPT" == *"-DTRAVIS"* ]]
172then
173  exit 1
174fi
175
176echo -e "Would you like to fix the format automatically (y/n): \c"
177
178# Make sure under any mode, we can read user input.
179exec < /dev/tty
180read to_fix
181
182if [ "$to_fix" != "y" ]
183then
184  exit 1
185fi
186
187# Do in-place format adjustment.
188if [ -z "$uncommitted_code" ]
189then
190  git diff -U0 "$FORMAT_UPSTREAM_MERGE_BASE" | $CLANG_FORMAT_DIFF -i -p 1
191else
192  git diff -U0 HEAD | $CLANG_FORMAT_DIFF -i -p 1
193fi
194echo "Files reformatted!"
195
196# Amend to last commit if user do the post-commit format check
197if [ -z "$uncommitted_code" ]; then
198  echo -e "Would you like to amend the changes to last commit (`git log HEAD --oneline | head -1`)? (y/n): \c"
199  read to_amend
200
201  if [ "$to_amend" == "y" ]
202  then
203    git commit -a --amend --reuse-message HEAD
204    echo "Amended to last commit"
205  fi
206fi
207