Skip to content

Instantly share code, notes, and snippets.

@sroccaserra
Last active February 1, 2022 10:51
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sroccaserra/174dbf0fedab9bf4e563667e5eb75565 to your computer and use it in GitHub Desktop.
Save sroccaserra/174dbf0fedab9bf4e563667e5eb75565 to your computer and use it in GitHub Desktop.
Analyser rapidement un répo de code

Git + Bash

Les âges des branches

$ git for-each-ref --sort=authordate --format '%(authordate:relative) %(refname:short)' refs/heads

Les 100 fichiers qui changent le plus souvent

Note : ne tient pas compte des renommages. Note : avec --before et --after on peut scanner une période donnée.

$ git log --format=format: --name-only --after=2020-01-01 | grep -v '^$' | sort | uniq -c | sort -r | head -100

Autre versions, nombre de commits par fichier :

$ find . -name "*.java" | xargs -n1 -I file sh -c 'echo `git log --oneline file | wc -l` file' | sort -nr
$ find . -name "*.java" -exec sh -c 'echo `git log --oneline {} | wc -l` {}' \; | sort -nr

Les 100 plus gros fichiers

Note : en nombre brut de lignes, même vides. Voir ci-dessous, avec Tokei pour compter les lignes de code source.

$ git ls-files | grep -v 'package.*\.json' | xargs wc -l | sort -r | head -100

Les fichiers qui ont changé récemment dans un répertoire donné

Note : l'étape de l'âge peut être assez longue. Sur cette étape, pv est optionnel, il sert à avoir un indicateur de progression.

$ git ls-files api/lib/domain/services > files.txt
 
$ git ls-files api/lib/domain/services | xargs -n 1 git log -1 --format='%ad' --date=short -- | pv -l > age.txt
 
$ paste age.txt files.txt | sort -r

Exemple qui combine ces trois données

Peut être utilisé pour générer un tsv, ou coller la sortie dans Excel.

Note : à exécuter à la racine du projet git, même pour analyser un sous-répertoire.

Note : à cause de la longueur de la commande pour déterminer la date de dernier changement, ça peut prendre plusieurs minutes. Restreindre à un sous répertoire accélère.

#!/usr/bin/env bash

set -e

SIZE_FILE=`mktemp /tmp/seb.XXXX`
AGE_FILE=`mktemp /tmp/seb.XXXX`
CHANGES_FILE=`mktemp /tmp/seb.XXXX`

DIR=${1:-.}

git log --format=format: --name-only "$DIR" | grep -v '^$' | sort | uniq -c > "$CHANGES_FILE"
git ls-files "$DIR" | xargs wc -l > "$SIZE_FILE"
git ls-files "$DIR" | xargs -n 1 git log -1 --format='%ad' --date=short -- | paste "$SIZE_FILE" - > "$AGE_FILE"


echo -e "file\tchanges\tsize\tage"
join -1 2 -2 2 "$CHANGES_FILE" "$AGE_FILE" | sort -rhk 2 | tr ' ' '\t'

rm "$SIZE_FILE" "$AGE_FILE" "$CHANGES_FILE"

Quels sont les fichiers ajoutés récemment dans ce répertoire

Note : c'est du fish shell, à adapter un brin si vous utilisez bash ou zsh.

$ for file in (find  lib/domain/services -type f) ; \
    git log --format=%ad --date=short $file | tail -1 ; \
    echo $file ; \
  end | \
  paste -d ' ' - - | \
  sort

Chercher les fonctions longues

Avec eslint et gawk, lister et trier par taille :

$ npx eslint -f compact --rule '"max-lines-per-function": ["error", 20]' lib |\
  gawk 'match($0, /\(([0-9]+)\)/, a) {print a[1]"\t"$0}' |\
  sort -rn

Avec un format un peu plus sympa à lire (autre option : écrire un formater eslint en JS) :

$ npx eslint -f compact --rule '"max-lines-per-function": ["error", 20]' lib |\
  gawk 'match($0, /([^:]+): line ([0-9]+).*Error - (.*) has too many lines \(([0-9]+)\)/, a) {print a[4]"\t"a[1]":"a[2]" "a[3]}' |\
  sort -rn |\
  less

La même chose mais filtre et ouvre les 20 premiers fichiers dans vim :

$ npx eslint -f compact --rule '"max-lines-per-function": ["error", 20]' lib |\
  gawk 'match($0, /([^:]+): line ([0-9]+).*Error - (.*) has too many lines \(([0-9]+)\)/, a) {sub(ENVIRON["PWD"]"/", "", a[1]); print a[4]"\t"a[1]":"a[2]" "a[3]}' |\
  grep -v 'Constructor\|serializer\|index\|module.exports = {' |\
  sort -rn |\
  head -20 |\
  awk '{printf $2"\0"}' |\
  xargs -o -0 vim

Avec un script awk qui compte le nombre de ligne par profondeur de blocs {} :

scopeDepth.awk

#!/usr/bin/env gawk

{ inc = 0; dec = 0 }
/{/ { inc = gsub(/{/, "{") }
/}/ { dec = gsub(/}/, "}") }
inc {
  for (i = depth + inc; i > depth; --i) {
    n[i] = 0;
    position[i] = FNR
    text[i] = $0
  }
}
dec {
  for (i = depth + inc; i > depth + inc - dec; --i) \
       if (n[i]> max_depth) print n[i]"\t"i"\t"FILENAME":"position[i]" "text[i];
}
{ depth += inc - dec; for (i = 0; i <= depth; ++i) n[i] += 1 }

Utilisation avec find et xargs pour analyser un répertoire :

find api/lib -type f -not -name '*.ods' -print0 |\
  xargs -0 -L10 awk -v max_depth=20 -f scopeDepth.awk |\
  grep -v 'class\|constructor\|index\|config\|serializer\|module.exports = {' |\
  sort -rn > result.txt

Lister les fonctions complexes avec ESLint

$ npx eslint -f compact --rule '"complexity": ["error", 5]' lib |\
  gawk 'match($0, /([^:]+): line ([0-9]+).*Error - (.*) has a complexity of ([0-9]+)/, a) {sub(ENVIRON["PWD"]"/","",a[1]);print a[4]"\t"a[1]":"a[2]" "a[3]}' |\
  sort -rn |\
  head -20

Les dépendances à la grosse louche

En JS par exemple, où le mot clé est require :

$ rg '\brequire\(\'\.' api/lib | awk -F\' '{ print $2 }' | awk -F/ '{ print $NF }' | sort | uniq -c | sort

Avec grep :

$ grep -Er '\brequire\(\'\.' api/lib | awk -F\' '{ print $2 }' | awk -F/ '{ print $NF }' | sort | uniq -c | sort

Archéologie avec git log

Pour voir tous les changements successifs d'un fichier :

$ git log -p monfichier.js

La même chose depuis Vim :

:!git log -p %

Trouver les commits dont le diff contient un texte (le fameux "git pickaxe") :

$ git log -p -S montexte

Trouver et dater les FIXME et autres TODO

rg --vimgrep --smart-case fixme | awk -F':' '{system("git --no-pager blame --show-name -L"$2","$2" "$1)}'

Avec grep :

grep --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=tmp --exclude-dir=.idea -rin fixme . | awk -F':' '{system("git --no-pager blame --show-name -L"$2","$2" "$1)}'

Avoir les short stats par date en TSV

Nécessite GNU Awk (gawk sur macOS). En Node.js les commits avec package-lock.json faussent les stats, on peut viser explicitement les répertoires sources (ici lib et tests). Pour voir si l’équipe fonctionne par “ajout de code” vs “transformation de code” par exemple ?

$ git log --shortstat --format='Date: %cd' --after=2020-11-15 lib tests | gawk '/Date:/ {d=$2}; match($0,/([0-9]+) insertions?/,a) {p=a[1]}; match($0,/([0-9]+) deletions?/,a) {m=a[1]}; /files? changed/ {print d"\t"p"\t"m}'

Ou dans un fichier awk pour sommer par date :

$ git log --format='Date: %cd' --shortstat --after=2020-01-01 lib tests | gawk -f stats.awk | sort > stats.tsv

Fichier stats.awk :

#!/usr/bin/env gawk -f

/^Date:/ { d = $2 ; dates[d]=1 }
match($0,/([0-9]+) insertions?/,a) { p[d] += a[1] }
match($0,/([0-9]+) deletions?/,a) { m[d] += a[1] }
END { for (d in dates) { print d"\t"p[d]"\t"m[d] } }

Avec Tokei

Les 20 fichiers les plus gros

Note : en lignes de code source.

tokei --sort=code --files (git ls-files | grep -v 'package.*\.json') | head -25

Rapport des lignes de code dans tous le projet, par langage

$ tokei --sort=code (git ls-files | grep -v 'package.*\.json')
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 JavaScript           1291        98096        74308         7606        16182
 Sass                  118         6986         5835          115         1036
 SVG                    64         2341         2315           26            0
 Handlebars            125         2606         2213           11          382
 Markdown                9         1812         1812            0            0
 YAML                    3          289          239           32           18
 HTML                    7          275          230            8           37
 Shell                  10          264          191           16           57
 Ruby                    1          249          179           26           44
 Plain Text              3           65           65            0            0
 JSON                    4           46           46            0            0
-------------------------------------------------------------------------------
 Total                1635       113029        87433         7840        17756
-------------------------------------------------------------------------------
%matplotlib notebook

from subprocess import run, PIPE
from datetime import datetime

import pandas as pd
import numpy as np

Récupérer les noms des fichiers

file_list_command = run(["git ls-files | grep -v 'package.*\.json'"], stdout=PIPE, shell=True)
file_list = file_list_command.stdout.decode('utf-8').rstrip().split('\n')

Récupérer les dates de dernière modification

Note : c'est un peu long.

file_age_command = run(["git ls-files | grep -v 'package.*\.json' | xargs -I '{}' git log -1 --format='%ad' --date=short -- '{}'"], stdout=PIPE, shell=True)
file_ages = file_age_command.stdout.decode('utf-8').rstrip().split('\n')

Récupérer les tailles des fichiers

file_size_command = run(["git ls-files | grep -v 'package.*\.json' | xargs wc -l | awk '{print $1}' | sed '$d'"], stdout=PIPE, shell=True)
file_sizes = file_size_command.stdout.decode('utf-8').rstrip().split('\n')

Aggréger les valeurs

df = pd.DataFrame({'file': file_list, 'age': file_ages, 'size': file_sizes})

Convertir en dates et trier

df['age'] = pd.to_datetime(df['age'])
df = df.sort_values('age', ascending=False)

Compter en jours depuis aujourd'hui

today_df = pd.to_datetime(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0))
df['age'] = today_df - df['age']

Ajouter l'âge en mois depuis aujourd'hui

df['age (month)'] = np.floor(df['age'].dt.days * 12 / 365)

Graph de répartition par génération

# Requires matplotlib

df['age (month)'].value_counts().sort_index().plot(kind='bar')
<IPython.core.display.Javascript object>

<matplotlib.axes._subplots.AxesSubplot at 0x1228a8cc0>

Réorganiser les colonnes et sauver au format Excel

# Nécessite openpyxl,
# pip install openpyxl

writer = pd.ExcelWriter('2018-10-16_file_age.xlsx')
df[['age', 'age (month)', 'size', 'file']].to_excel(writer, index=False)
writer.save()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment