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