Repository: so-fancy/diff-so-fancy
Branch: next
Commit: 503456ec17d1
Files: 61
Total size: 145.6 KB
Directory structure:
gitextract_6y16t5y0/
├── .circleci/
│ └── config.yml
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── awesomebot.yml
│ └── publish.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── diff-so-fancy
├── diff-so-fancy.plugin.zsh
├── docs/
│ └── diff-so-fancy.1
├── hacking-and-testing.md
├── history.md
├── lib/
│ └── DiffHighlight.pm
├── package.json
├── pro-tips.md
├── report-bug.sh
├── reporting-bugs.md
├── test/
│ ├── bugs.bats
│ ├── diff-so-fancy.bats
│ ├── fixtures/
│ │ ├── add_empty_file.diff
│ │ ├── add_remove_empty_lines.diff
│ │ ├── ansi_reset_no_number.diff
│ │ ├── binary-modified.diff
│ │ ├── chromium-modaltoelement.diff
│ │ ├── complex-hunks.diff
│ │ ├── diff_recursive.diff
│ │ ├── dotfiles.diff
│ │ ├── file-moves.diff
│ │ ├── file-perms.diff
│ │ ├── file-rename.diff
│ │ ├── file_copy.diff
│ │ ├── file_with_space.diff
│ │ ├── first-three-line.diff
│ │ ├── hg.diff
│ │ ├── hunk_no_comma.diff
│ │ ├── latin1.diff
│ │ ├── leading-dashes.diff
│ │ ├── ls-function.diff
│ │ ├── mnemonicprefix.diff
│ │ ├── move_with_content_change.diff
│ │ ├── noprefix.diff
│ │ ├── recursive_default_as_mercurial.diff
│ │ ├── recursive_longhand_as_mercurial.diff
│ │ ├── remove_empty_file.diff
│ │ ├── remove_slashn_eof.diff
│ │ ├── single-line-remove.diff
│ │ ├── truecolor.diff
│ │ ├── unicode.diff
│ │ └── weird.diff
│ ├── git-config.bats
│ ├── git_ansi_color.pl
│ └── test_helper/
│ └── util.bash
├── third_party/
│ ├── ansi-reveal/
│ │ └── ansi-reveal.pl
│ ├── build_fatpack/
│ │ └── build.pl
│ ├── cli_bench/
│ │ └── cli_bench.pl
│ └── term-colors/
│ └── term-colors.pl
└── update-deps.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
machine: true
steps:
- checkout
- run:
name:
command: |
sudo apt-get -y update
sudo apt-get -y install shellcheck git
- run: git submodule sync
- run: git submodule update --init
- run:
name: tests
command: |
export TERM=dumb && ./test/bats/bin/bats test
shellcheck *.sh
================================================
FILE: .github/dependabot.yml
================================================
---
# Use `allow` to specify which dependencies to maintain
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/awesomebot.yml
================================================
---
name: Check links in README.md
on:
# Run daily
schedule: [{cron: "0 1 * * *"}]
push:
branches: ["*"]
pull_request:
branches: ["*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: docker://dkhamsing/awesome_bot:latest
with:
args: /github/workspace/README.md --allow-timeout --allow 202,206,403,429,500,501,502,503,504,509,521,522 --allow-dupe --allow-ssl --request-delay 1 --allow-redirect --white-list https://ipfs.io,slideshare,https://img.shields.io,https://codeclimate.com/,https://www.concourse.ci
# args: /github/workspace/README.md --allow-timeout --allow 202,206,403,429,500,501,502,503,504,509,521,522 --allow-dupe --allow-ssl --request-delay 1 --allow-redirect --white-list https://ipfs.io,slideshare,https://img.shields.io,https://codeclimate.com/github/unixorn/awesome-zsh-plugins,www-s.acm.illinois.edu,https://mgdm.net,https://www.concourse.ci,https://grml.org/zsh/zsh-lovers.html,https://geeknote.me,https://en.ipip.net,https://docs.virtuozzo.com,kubernetes.io,https://youtube-dl.org,https://1password.com,https://iterm2.com,https://mercurial-scm.org,https://hitokoto.cn,https://www.cyberciti.biz,https://keybase.io,https://exercism.io,https://bitbucket.org,https://code.visualstudio.com,https://www.gnu.org
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Perl and App::FatPacker
run: |
sudo apt-get update
sudo apt-get install -y perl cpanminus
cpanm --local-lib=~/perl5 App::FatPacker
echo "PATH=$HOME/perl5/bin:$PATH" >> $GITHUB_ENV
echo "PERL5LIB=$HOME/perl5/lib/perl5:$PERL5LIB" >> $GITHUB_ENV
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.14.0
registry-url: 'https://registry.npmjs.org'
- name: Install and Build
run: |
npm ci
npm run build
- name: Publish to npm
run: |
npm install -g npm@latest
npm publish --provenance --access public --registry https://registry.npmjs.org/
================================================
FILE: .gitignore
================================================
/dist/
================================================
FILE: .gitmodules
================================================
[submodule "test/bats"]
path = test/bats
url = https://github.com/bats-core/bats-core.git
[submodule "test/test_helper/bats-support"]
path = test/test_helper/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "test/test_helper/bats-assert"]
path = test/test_helper/bats-assert
url = https://github.com/bats-core/bats-assert.git
================================================
FILE: .npmignore
================================================
/test/
================================================
FILE: .travis.yml
================================================
os: linux
language: perl
perl:
- blead
- dev
- 5.30
- 5.28
- 5.26
- 5.24
- 5.22
- 5.20
- 5.18
- 5.14
matrix:
include:
- os: osx
osx_image: xcode12.2
language: generic
perl: 5.30
allow_failures:
- perl: "blead"
addons:
homebrew:
packages:
- perl
- cpanminus
update: true
before_install:
- git submodule sync
- git submodule update --init
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
mkdir -p ~/perl5;
fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
eval $(curl https://travis-perl.github.io/init) --perl;
sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";
sudo apt-get -y update;
fi
install:
- cpanm --quiet --notest --local-lib=~/perl5 local::lib
- eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
- cpanm --quiet --notest Test::Perl::Critic Perl::Critic::Freenode
script:
- perlcritic -1 -q --theme freenode diff-so-fancy
- ./test/bats/bin/bats test
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 So Fancy team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# 🎩 diff-so-fancy [](https://circleci.com/gh/so-fancy/diff-so-fancy) [](https://ci.appveyor.com/project/stevemao/diff-so-fancy/branch/master)
`diff-so-fancy` makes your diffs **human** readable instead of machine readable. This helps improve code quality and helps you spot defects faster.
## 🖼️ Screenshot
Vanilla `git diff` vs `git` and `diff-so-fancy`

## 📦 Install
Simply copy the `diff-so-fancy` script from the latest Github release into your `$PATH` and you're done. Alternately to test development features you can clone this repo and then put the `diff-so-fancy` script (symlink will work) into your `$PATH`. The `lib/` directory will need to be kept relative to the core script.
`diff-so-fancy` is also available from the [NPM registry](https://www.npmjs.com/package/diff-so-fancy), [brew](https://formulae.brew.sh/formula/diff-so-fancy), [Fedora](https://packages.fedoraproject.org/pkgs/diff-so-fancy/diff-so-fancy/), in the [Arch extra repo](https://archlinux.org/packages/extra/any/diff-so-fancy/), and as [ppa:aos for Debian/Ubuntu Linux](https://github.com/aos/dsf-debian).
Issues relating to packaging ("installation does not work", "version is out of date", etc.) should be directed to those packages' repositories/issue trackers where applicable.
## ✨ Usage
### Git
Configure git to use `diff-so-fancy` for all diff output:
```shell
git config --global core.pager "diff-so-fancy | less --tabs=4 -RF"
git config --global interactive.diffFilter "diff-so-fancy --patch"
```
### Diff
Use `-u` with `diff` for unified output, and pipe the output to `diff-so-fancy`:
```shell
diff -u file_a file_b | diff-so-fancy
```
We also support recursive mode with `-r` or `--recursive`
```shell
diff --recursive -u /path/folder_a /path/folder_b | diff-so-fancy
```
## ⚒️ Options
### markEmptyLines
Colorize the first block of an empty line. (Default: true)
```shell
git config --bool --global diff-so-fancy.markEmptyLines false
```
### changeHunkIndicators
Simplify Git header chunks to a human readable format. (Default: true)
```shell
git config --bool --global diff-so-fancy.changeHunkIndicators false
```
### stripLeadingSymbols
Should the `+` or `-` symbols at line-start be removed. (Default: true)
```shell
git config --bool --global diff-so-fancy.stripLeadingSymbols false
```
### useUnicodeRuler
By default, the separator for the file header uses Unicode line-drawing characters.
If this is causing output errors on your terminal, set this to `false` to use
ASCII characters instead. (Default: true)
```shell
git config --bool --global diff-so-fancy.useUnicodeRuler false
```
### rulerWidth
By default, the separator for the file header spans the full width of the terminal.
Use rulerWidth to set the width of the file header manually.
```shell
git config --global diff-so-fancy.rulerWidth 80
```
## 👨 The diff-so-fancy team
| Person | Role |
| --------------------- | ---------------- |
| @scottchiefbaker | Project lead |
| @OJFord | Bug triage |
| @GenieTim | Travis OSX fixes |
| @AOS | Debian packager |
| @Stevemao/@Paul Irish | NPM release team |
## 🧬 Contributing
Pull requests are quite welcome, and should target the
[`next` branch](https://github.com/so-fancy/diff-so-fancy/tree/next). We are also
looking for any feedback or ideas on how to make `diff-so-fancy` even *fancier*.
### Other documentation
* [Pro-tips for advanced users](pro-tips.md)
* [Reporting Bugs](reporting-bugs.md)
* [Hacking and Testing](hacking-and-testing.md)
* [History](history.md)
## 🔃 Alternatives
* [Delta](https://github.com/dandavison/delta)
* [Lazygit](https://github.com/jesseduffield/lazygit) with diff-so-fancy [integration](https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff-so-fancy)
## 🏛️ License
MIT
================================================
FILE: appveyor.yml
================================================
build: false
before_test:
- git submodule sync
- git submodule update --init
test_script:
- C:\cygwin\bin\bash -lc "cd $APPVEYOR_BUILD_FOLDER; ./test/bats/bin/bats test"
================================================
FILE: diff-so-fancy
================================================
#!/usr/bin/env perl
my $VERSION = "1.4.7";
#################################################################################
use v5.014; # Require Perl 5.14 for 'state' variables and /u in regexes
use warnings FATAL => 'all';
use strict;
use File::Spec; # For catdir
use File::Basename; # For dirname
use Cwd qw(abs_path); # For realpath()
use lib dirname(abs_path(File::Spec->catdir($0))) . "/lib"; # Add the local lib/ to @INC
use DiffHighlight;
my $remove_file_add_header = 1;
my $remove_file_delete_header = 1;
my $clean_permission_changes = 1;
my $patch_mode = 0;
my $manually_color_lines = 0; # Usually git/hg colorizes the lines, but for raw patches we use this
my $change_hunk_indicators = git_config_boolean("diff-so-fancy.changeHunkIndicators","true");
my $strip_leading_indicators = git_config_boolean("diff-so-fancy.stripLeadingSymbols","true");
my $mark_empty_lines = git_config_boolean("diff-so-fancy.markEmptyLines","true");
my $use_unicode_dash_for_ruler = git_config_boolean("diff-so-fancy.useUnicodeRuler","true");
my $ruler_width = git_config("diff-so-fancy.rulerWidth", undef);
my $git_strip_prefix = git_config_boolean("diff.noprefix","false");
my $has_stdin = has_stdin();
my $ansi_regex = qr/\e\[([0-9]{0,3}(;[0-9]{1,3}){0,10})[mK]/;
my $ansi_color_regex = qr/(${ansi_regex})?/;
my $reset_color = color("reset");
my $bold = color("bold");
my $meta_color = "";
# Set the diff highlight colors from the config
init_diff_highlight_colors();
my ($file_1,$file_2);
my $args = argv(); # Hashref of all the ARGV stuff
my $last_file_seen = "";
my $last_file_mode = "";
my $i = 0;
my $in_hunk = 0;
my $columns_to_remove = 0;
my $is_mercurial = 0;
my $color_forced = 0; # Has the color been forced on/off
if ($args->{rulerWidth}) {
$ruler_width = int($args->{rulerWidth});
}
# We try and be smart about whether we need to do line coloring, but
# this is an option to force it on/off
if ($args->{color_on}) {
$manually_color_lines = 1;
$color_forced = 1;
} elsif ($args->{color_off}) {
$manually_color_lines = 0;
$color_forced = 1;
}
if ($args->{debug}) {
show_debug_info();
}
# We only process ARGV if we don't have STDIN
if (!$has_stdin) {
if ($args->{v} || $args->{version}) {
die(version());
} elsif ($args->{'set-defaults'}) {
my $ok = set_defaults();
exit;
} elsif ($args->{colors}) {
# We print this to STDOUT so we can redirect to bash to auto-set the colors
print get_default_colors();
exit;
} elsif (!%$args || $args->{help} || $args->{h}) {
die(usage());
} else {
die("Missing input on STDIN\n");
}
}
#################################################################################
#################################################################################
# The logic here is that we run all the lines through DiffHighlight first. This
# highlights all the intra-word changes. Then we take those lines and send them
# to do_dsf_stuff() to convert the diff to human readable d-s-f output and add
# appropriate fanciness
my @lines;
local $DiffHighlight::line_cb = sub {
push(@lines,@_);
# Wait until we have enough lines to process, then divide up into safe
# parseable chunks and have d-s-f do it's magic
if (@lines > 100) {
my @chunks = get_diff_chunks(\@lines);
@lines = ();
foreach my $chunk (@chunks) {
do_dsf_stuff($chunk);
}
}
};
my $line_count = 0;
while (my $line = <STDIN>) {
# If the very first line of the diff doesn't start with ANSI color we're assuming
# it's a raw patch file, and we have to color the added/removed lines ourself
if (!$color_forced && $line_count == 0 && !starts_with_ansi($line)) {
$manually_color_lines = 1;
}
my $ok = DiffHighlight::handle_line($line);
$line_count++;
}
# If we're mid hunk above process anything still pending
DiffHighlight::flush();
# Process anything left over
my @chunks = get_diff_chunks(\@lines);
foreach my $chunk (@chunks) {
do_dsf_stuff($chunk);
}
#################################################################################
#################################################################################
sub do_dsf_stuff {
my $input = shift();
# If we're in debug mode we want to log the chunks we see
if ($args->{debug}) { log_chunks($input); }
# FIXME: There is a scenario where there are FIVE diff header lines
# I'm not sure if we need to account for that or not. Unit tests pass so... bees?
my $cnt = count_git_header_lines(@$input);
if ($cnt == 4) {
$patch_mode = 1;
}
# Calculate the context lines for this chunk
my $context_lines = calculate_context_lines(@$input);
#print STDERR "START -------------------------------------------------\n";
#print STDERR join("",@$input);
#print STDERR "END ---------------------------------------------------\n";
# We track if the last '^diff' line had recursive specified
my $recursive = 0;
while (my $line = shift(@$input)) {
######################################################
# Pre-process the line before we do any other markup #
######################################################
# If the first line of the input is a blank line, skip that
if ($i == 0 && $line =~ /^\s*$/) {
next;
}
######################
# End pre-processing #
######################
#######################################################################
####################################################################
# Look for git index and replace it horizontal line (header later) #
####################################################################
if ($line =~ /^${ansi_color_regex}index /) {
# Print the line color and then the actual line
$meta_color = $1 || get_config_color("meta");
# Get the next line without incrementing counter while loop
my $next = $input->[0] || "";
my ($file_1,$file_2);
# The line immediately after the "index" line should be the --- file line
# If it's not it's an empty file add/delete
if ($next !~ /^$ansi_color_regex(---|Binary files)/) {
# We fake out the file names since it's a raw add/delete
if ($last_file_mode eq "add") {
$file_1 = "/dev/null";
$file_2 = $last_file_seen;
} elsif ($last_file_mode eq "delete") {
$file_1 = $last_file_seen;
$file_2 = "/dev/null";
}
}
if ($file_1 && $file_2) {
print horizontal_rule($meta_color);
print $meta_color . file_change_string($file_1,$file_2) . "\n";
print horizontal_rule($meta_color);
}
#########################
# Look for the filename #
#########################
# $4
} elsif ($line =~ /^${ansi_color_regex}diff (.+?)$/) {
my $extra = $4;
# Mercurial looks like: diff --recursive -u core/app.py language/app.py
if ($extra =~ m/(-r|--recursive) -u (.+?) (.*)/) {
$is_mercurial = 1;
$meta_color = get_config_color("meta");
$last_file_seen = basename($3 || "");
$recursive = 1;
# Git looks like: diff --git a/diff-so-fancy b/diff-so-fancy
} elsif ($extra =~ m/--git/) {
# Note file may contains spaces
my ($a_file, $b_file) = $extra =~ m/(a\/.+) (b\/.+)/;
$a_file ||= "";
$b_file ||= "";
$last_file_seen = $a_file;
$recursive = 0;
}
$last_file_seen =~ s|^\w/||; # Remove a/ (and handle diff.mnemonicPrefix).
$in_hunk = 0;
if ($patch_mode) {
# we are consuming one line, and the debt must be paid
print "\n";
}
########################################
# Find the first file: --- a/README.md #
########################################
} elsif (!$in_hunk && $line =~ /^$ansi_color_regex--- (\w\/)?(.+?)(\e|\t|$)/) {
$meta_color = get_config_color("meta");
if ($git_strip_prefix) {
my $file_dir = $4 || "";
$file_1 = $file_dir . $5;
} else {
$file_1 = $5;
}
# Find the second file on the next line: +++ b/README.md
my $next = shift(@$input) || "";
$next =~ /^$ansi_color_regex\+\+\+ (\w\/)?(.+?)(\e|\t|$)/;
if ($1) {
print $1; # Print out whatever color we're using
}
if ($git_strip_prefix) {
my $file_dir = $4 || "";
$file_2 = $file_dir . $5;
} else {
$file_2 = $5;
}
if ($file_2 ne "/dev/null") {
$last_file_seen = $file_2;
}
# In recursive mode we massage the file names so they're similar
if ($recursive) {
# In recursive the files have the date appended:
# --- /var/tmp/a/index.txt 2026-03-13 21:35:20.997231861 -0700
# so we remove the date portion
$file_1 =~ s/\s+(\d{4}-\d{2}-\d{2}.+)//;
$file_2 =~ s/\s+(\d{4}-\d{2}-\d{2}.+)//;
my $common = common_prefix($file_1, $file_2);
# Remove the common prefix from each as well as the next dir
$file_1 =~ s/$common.+?\///;
$file_2 =~ s/$common.+?\///;
}
# Print out the top horizontal line of the header
print $reset_color;
print horizontal_rule($meta_color);
# Mercurial coloring is slightly different so we need to hard reset colors
if ($is_mercurial) {
print $reset_color;
}
print $meta_color;
print file_change_string($file_1,$file_2) . "\n";
# Print out the bottom horizontal line of the header
print horizontal_rule($meta_color);
########################################
# Check for "@@ -3,41 +3,63 @@" syntax #
########################################
} elsif (!$change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
$in_hunk = 1;
print $line;
} elsif ($change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
$in_hunk = 1;
my $hunk_header = $4;
my $remain = bleach_text($5);
# The number of colums to remove (1 or 2) is based on how many commas in the hunk header
$columns_to_remove = (char_count(",",$hunk_header)) - 1;
# On single line removes there is NO comma in the hunk so we force one
if ($columns_to_remove <= 0) {
$columns_to_remove = 1;
}
if ($1) {
print $1; # Print out whatever color we're using
}
my ($orig_offset, $orig_count, $new_offset, $new_count) = parse_hunk_header($hunk_header);
#$last_file_seen = basename($last_file_seen);
# Figure out the start line
my $start_line = start_line_calc($new_offset, $new_count, $context_lines);
# Last function has it's own color
my $last_function_color = "";
if ($remain) {
$last_function_color = get_config_color("last_function");
}
# Check to see if we have the color for the fragment from git
if ($5 =~ /\e\[\d/) {
#print "Has ANSI color for fragment\n";
} else {
# We don't have the ANSI sequence so we shell out to get it
#print "No ANSI color for fragment\n";
my $frag_color = get_config_color("fragment");
print $frag_color;
}
print "@ $last_file_seen:$start_line \@${reset_color}${last_function_color}${remain}${reset_color}\n";
###################################
# Remove any new file permissions #
###################################
} elsif ($remove_file_add_header && $line =~ /^${ansi_color_regex}new file mode [0-7]{6}/) {
# Don't print the line (i.e. remove it from the output);
$last_file_mode = "add";
if ($patch_mode) {
print "\n";
}
######################################
# Remove any delete file permissions #
######################################
} elsif ($remove_file_delete_header && $line =~ /^${ansi_color_regex}deleted file mode [0-7]{6}/) {
# Don't print the line (i.e. remove it from the output);
$last_file_mode = "delete";
if ($patch_mode) {
print "\n";
}
################################
# Look for binary file changes #
################################
} elsif ($line =~ /^Binary files (\w\/)?(.+?) and (\w\/)?(.+?) differ/) {
my $change = file_change_string($2,$4);
print horizontal_rule($meta_color);
print "$meta_color$change (binary)\n";
print horizontal_rule($meta_color);
#####################################################
# Check if we're changing the permissions of a file #
#####################################################
} elsif ($clean_permission_changes && $line =~ /^${ansi_color_regex}old mode (\d+)/) {
my ($old_mode) = $4;
my $next = shift(@$input);
if ($1) {
print $1; # Print out whatever color we're using
}
my ($new_mode) = $next =~ m/new mode (\d+)/;
if ($patch_mode) {
print "\n";
}
print "$last_file_seen changed file mode from $old_mode to $new_mode\n";
###############
# File rename #
###############
} elsif ($line =~ /^${ansi_color_regex}similarity index (\d+)%/) {
my $simil = $4;
# If it's a move with content change we ignore this and the next two lines
if ($simil != 100) {
shift(@$input);
shift(@$input);
next;
}
my $next = shift(@$input);
my ($action1, $file1) = $next =~ /(copy|rename) from (.+?)(\e|\t|$)/;
$next = shift(@$input);
my ($action2, $file2) = $next =~ /(copy|rename) to (.+?)(\e|\t|$)/;
if ($file1 && $file2) {
# We may not have extracted this yet, so we pull from the config if not
$meta_color = get_config_color("meta");
my $change = "???";
if ($action1 eq "copy") {
$change = "Copied $file1 to $file2";
} else {
$change = file_change_string($file1,$file2);
}
print horizontal_rule($meta_color);
print $meta_color . $change . "\n";
print horizontal_rule($meta_color);
}
$i += 3; # We've consumed three lines
next;
#####################################
# Just a regular line, print it out #
#####################################
} else {
# Mark empty line with a red/green box indicating addition/removal
if ($mark_empty_lines) {
$line = mark_empty_line($line);
}
# Remove the correct number of leading " " or "+" or "-"
if ($strip_leading_indicators) {
$line = strip_leading_indicators($line,$columns_to_remove);
}
print $line;
}
$i++;
}
}
######################################################################################################
# End regular code, begin functions
######################################################################################################
# Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805
sub parse_hunk_header {
my ($line) = @_;
my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = $line =~ /^\@\@+(?: -(\d+)(?:,(\d+))?)+ \+(\d+)(?:,(\d+))? \@\@+/;
$o_cnt = 1 unless defined $o_cnt;
$n_cnt = 1 unless defined $n_cnt;
return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
}
# Mark the first char of an empty line
sub mark_empty_line {
my $line = shift();
my $reset_color = "\e\\[0?m";
my $reset_escape = "\e\[m";
my $invert_color = "\e\[7m";
my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1];
my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1];
# This captures lines that do not have any ANSI in them (raw vanilla diff)
if ($line eq "+\n") {
$line = $invert_color . $add_color . " " . color('reset') . "\n";
# This captures lines that do not have any ANSI in them (raw vanilla diff)
} elsif ($line eq "-\n") {
$line = $invert_color . $del_color . " " . color('reset') . "\n";
# This handles everything else
} else {
$line =~ s/^($ansi_color_regex)[+-]$reset_color\s*$/$invert_color$1 $reset_escape\n/;
}
return $line;
}
# String to boolean
sub boolean {
my $str = shift();
$str = trim($str);
if ($str eq "" || $str =~ /^(no|false|0)$/i) {
return 0;
} else {
return 1;
}
}
# Get the git config
sub git_config_raw {
my $cmd = "git config --list 2>&1";
my @out = `$cmd`;
return \@out;
}
# Memoize fetching a textual item from the git config
sub git_config {
my $search_key = lc($_[0] || "");
my $default_value = lc($_[1] || "");
state $raw = {};
if (%$raw && $search_key) {
return $raw->{$search_key} || $default_value;
}
if ($args->{debug}) {
print "Parsing git config\n";
}
my $out = git_config_raw();
foreach my $line (@$out) {
if ($line =~ /=/) {
my ($key,$value) = split("=",$line,2);
$value =~ s/\s+$//;
$raw->{$key} = $value;
}
}
# If we're given a search key return that, else return the hash
if ($search_key) {
return $raw->{$search_key} || $default_value;
} else {
return $raw;
}
}
# Fetch a boolean item from the git config
sub git_config_boolean {
my $search_key = lc($_[0] || "");
my $default_value = lc($_[1] || 0); # Default to false
my $result = git_config($search_key,$default_value);
my $ret = boolean($result);
return $ret;
}
sub get_less_charset {
my @less_char_vars = ("LESSCHARSET", "LESSCHARDEF", "LC_ALL", "LC_CTYPE", "LANG");
foreach my $key (@less_char_vars) {
my $val = $ENV{$key};
if (defined $val) {
return ($key, $val);
}
}
return ();
}
sub should_print_unicode {
if (-t STDOUT) {
# Always print unicode chars if we're not piping stuff, e.g. to less(1)
return 1;
}
# Otherwise, assume we're piping to less(1)
my ($less_env_var, $less_charset) = get_less_charset();
if ($less_charset && $less_charset =~ /utf-?8/i) {
return 1;
}
return 0;
}
# Try and be smart about what line the diff hunk starts on
sub start_line_calc {
my ($line_num, $diff_context, $context_lines) = @_;
my $ret;
if ($line_num == 0 && $diff_context == 0) {
return 1;
}
# Three lines on either side, and the line itself = 7
my $expected_context = ($context_lines * 2 + 1);
# The first three lines
if ($line_num == 1 && $diff_context < $expected_context) {
$ret = $diff_context - $context_lines;
} else {
$ret = $line_num + $context_lines;
}
if ($ret < 1) {
$ret = 1;
}
return $ret;
}
# Remove + or - at the beginning of the lines
sub strip_leading_indicators {
my $line = shift(); # Array passed in by reference
my $columns_to_remove = shift(); # Don't remove any lines by default
if ($columns_to_remove == 0) {
return $line; # Nothing to do
}
$line =~ s/^(${ansi_color_regex})([ +-]){${columns_to_remove}}/$1/;
if ($manually_color_lines) {
if (defined($5) && $5 eq "+") {
my $add_line_color = get_config_color("add_line");
$line = $add_line_color . insert_reset_at_line_end($line);
} elsif (defined($5) && $5 eq "-") {
my $remove_line_color = get_config_color("remove_line");
$line = $remove_line_color . insert_reset_at_line_end($line);
}
}
return $line;
}
# Insert the color reset code at end of line, but before any newlines
sub insert_reset_at_line_end {
my $line = shift();
$line =~ s/^(.*)([\n\r]+)?$/${1}${reset_color}${2}/;
return $line;
}
# Count the number of a given char in a string
# https://www.perturb.org/display/1010_Perl_Count_occurrences_of_substring.html
sub char_count {
my ($needle, $haystack) = @_;
my $count = () = ($haystack =~ /$needle/g);
return $count;
}
# Remove all ANSI codes from a string
sub bleach_text {
my $str = shift();
$str =~ s/\e\[\d*(;\d+)*m//mg;
return $str;
}
# Remove all trailing and leading spaces
sub trim {
my $s = shift();
if (!$s) { return ""; }
$s =~ s/^\s*//u;
$s =~ s/\s*$//u;
return $s;
}
# Print a line of em-dash or line-drawing chars the full width of the screen
sub horizontal_rule {
my $color = $_[0] || "";
my $width = get_terminal_width();
# em-dash http://www.fileformat.info/info/unicode/char/2014/index.htm
#my $dash = "\x{2014}";
# BOX DRAWINGS LIGHT HORIZONTAL http://www.fileformat.info/info/unicode/char/2500/index.htm
my $dash;
if ($use_unicode_dash_for_ruler && should_print_unicode()) {
#$dash = Encode::encode('UTF-8', "\x{2500}");
$dash = "\xE2\x94\x80";
} else {
$dash = "-";
}
# Draw the line
my $ret = $color . ($dash x $width) . "$reset_color\n";
return $ret;
}
sub file_change_string {
my $file_1 = shift();
my $file_2 = shift();
# If they're the same it's a modify
if ($file_1 eq $file_2) {
return "modified: $file_1";
# If the first is /dev/null it's a new file
} elsif ($file_1 eq "/dev/null") {
my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1];
return "added: $add_color$file_2$reset_color";
# If the second is /dev/null it's a deletion
} elsif ($file_2 eq "/dev/null") {
my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1];
return "deleted: $del_color$file_1$reset_color";
# If the files aren't the same it's a rename
} elsif ($file_1 ne $file_2) {
my ($old, $new) = DiffHighlight::highlight_pair($file_1,$file_2,{only_diff => 1});
# highlight_pair already includes reset_color, but adds newline characters that need to be trimmed off
$old = trim($old);
$new = trim($new);
return "renamed: $old$meta_color to $new"
# Something we haven't thought of yet
} else {
return "$file_1 -> $file_2";
}
}
# Check to see if STDIN is connected to an interactive terminal
sub has_stdin {
my $i = -t STDIN;
my $ret = int(!$i);
return $ret;
}
# We use this instead of Getopt::Long because it's faster and we're not parsing any
# crazy arguments
# Borrowed from: https://www.perturb.org/display/1153_Perl_Quick_extract_variables_from_ARGV.html
sub argv {
my $ret = {};
for (my $i = 0; $i < scalar(@ARGV); $i++) {
# If the item starts with "-" it's a key
if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_-]*\w)$/) && ($ARGV[$i] !~ /^-\w\w/)) {
# If the next item does not start with "--" it's the value for this item
if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
$ret->{$key} = $ARGV[$i + 1];
# Bareword like --verbose with no options
} else {
$ret->{$key}++;
}
}
}
# We're looking for a certain item
if ($_[0]) { return $ret->{$_[0]}; }
return $ret;
}
# Output the command line usage for d-s-f
sub usage {
my $out = color("white_bold") . version() . color("reset") . "\n";
$out .= "Usage:
git diff --color | diff-so-fancy # Use d-s-f on one diff
cat diff.txt | diff-so-fancy # Use d-s-f on a diff/patch file
diff -u one.txt two.txt | diff-so-fancy # Use d-s-f on unified diff output
diff-so-fancy --colors # View the commands to set the recommended colors
diff-so-fancy --set-defaults # Configure git-diff to use diff-so-fancy and suggested colors
diff-so-fancy --patch # Use diff-so-fancy in patch mode (interoperable with `git add --patch`)
# Configure git to use d-s-f for *all* diff operations
git config --global core.pager \"diff-so-fancy | less --tabs=4 -RFX\"
# Configure git to use d-s-f for `git add --patch`
git config --global interactive.diffFilter \"diff-so-fancy --patch\"\n";
return $out;
}
sub get_default_colors {
my $out = "# Recommended default colors for diff-so-fancy\n";
$out .= "# --------------------------------------------\n";
$out .= 'git config --global color.ui true
git config --global color.diff-highlight.oldNormal "red bold"
git config --global color.diff-highlight.oldHighlight "red bold 52"
git config --global color.diff-highlight.newNormal "green bold"
git config --global color.diff-highlight.newHighlight "green bold 22"
git config --global color.diff.meta "yellow"
git config --global color.diff.frag "magenta bold"
git config --global color.diff.commit "yellow bold"
git config --global color.diff.old "red bold"
git config --global color.diff.new "green bold"
git config --global color.diff.whitespace "red reverse"
';
return $out;
}
# Output the current version string
sub version {
my $ret = "Diff-so-fancy: https://github.com/so-fancy/diff-so-fancy\n";
$ret .= "Version : $VERSION\n";
return $ret;
}
sub is_windows {
if ($^O eq 'MSWin32' or $^O eq 'dos' or $^O eq 'os2' or $^O eq 'cygwin' or $^O eq 'msys') {
return 1;
} else {
return 0;
}
}
sub set_defaults {
my $color_config = get_default_colors();
my $git_config = 'git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"';
my $first_cmd = 'git config --global diff-so-fancy.first-run false';
my @cmds = split(/\n/,$color_config);
push(@cmds,$git_config);
push(@cmds,$first_cmd);
# Remove all comments from the commands
foreach my $x (@cmds) {
$x =~ s/#.*//g;
}
# Remove any empty commands
@cmds = grep($_,@cmds);
foreach my $cmd (@cmds) {
system($cmd);
my $exit = ($? >> 8);
if ($exit != 0) {
die("Error running: '$cmd' (error #18941)\n");
}
}
return 1;
}
# Borrowed from: https://www.perturb.org/display/1167_Perl_ANSI_colors.html
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
my ($str, $txt) = @_;
# If we're NOT connected to a an interactive terminal don't do color
#if (-t STDOUT == 0) { return ''; }
# No string sent in, so we just reset
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
# Some predefined colors
my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
# Get foreground/background and any commands
my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
my ($bc) = $str =~ /on_(\d{1,3})$/g;
# Some predefined commands
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
my $cmd_num = $cmd_map{$cmd // 0};
my $ret = '';
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
if ($txt) { $ret .= $txt . "\e[0m"; }
return $ret;
}
# Get colors used for various output sections (memoized)
sub get_config_color {
my $str = shift();
state $static_config;
my $ret = "";
if ($static_config->{$str}) {
return $static_config->{$str};
}
#print color(15) . "Shelling out for color: '$str'\n" . color('reset');
if ($str eq "meta") {
# Default ANSI yellow
$ret = git_ansi_color(git_config('color.diff.meta')) || color(11);
} elsif ($str eq "reset") {
$ret = color("reset");
} elsif ($str eq "add_line") {
# Default ANSI green
$ret = git_ansi_color(git_config('color.diff.new')) || color("2_bold");
} elsif ($str eq "remove_line") {
# Default ANSI red
$ret = git_ansi_color(git_config('color.diff.old')) || color("1_bold");
} elsif ($str eq "fragment") {
$ret = git_ansi_color(git_config('color.diff.frag')) || color("13_bold");
} elsif ($str eq "last_function") {
$ret = git_ansi_color(git_config('color.diff.func')) || color("146_bold");
}
# Cache (memoize) the entry for later
$static_config->{$str} = $ret;
return $ret;
}
# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
sub git_ansi_color {
my $str = shift();
my @parts = split(' ', $str);
if (!@parts) {
return '';
}
my $colors = {
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9, # pseudo color (39/49 = set default)
'normal' => -1, # placeholder color to be ignored
};
# Bright colors are just offsets from the "regular" color
for my $k (keys %{ $colors }) {
$colors->{"bright" . $k} = $colors->{$k} + 60;
}
my @ansi_part = ();
if (grep { /^bold$/ } @parts) {
push(@ansi_part, "1");
}
if (grep { /^dim$/ } @parts) {
push(@ansi_part, "2");
}
if (grep { /^ul$/ } @parts) {
push(@ansi_part, "4");
}
if (grep { /^reverse$/ } @parts) {
push(@ansi_part, "7");
}
# Remove parts that aren't colors
@parts = grep { exists $colors->{$_} || is_numeric($_) || /^\#/ } @parts;
my $fg = $parts[0] // "";
my $bg = $parts[1] // "";
set_ansi_color($fg, 0 , \@ansi_part, $colors) if $fg;
set_ansi_color($bg, 10, \@ansi_part, $colors) if $bg;
#############################################
my $ansi_str = join(";", @ansi_part);
my $ret = "\e[" . $ansi_str . "m";
return $ret;
}
sub set_ansi_color {
my ($color, $increment, $ansi_part, $colors) = @_;
my $base_code = 30 + $increment;
my $base8_code = 38 + $increment;
my $ext_code = 82 + $increment;
if (is_numeric($color)) {
if ($color < 8) {
push(@$ansi_part, $color + $base_code);
} elsif ($color < 16) {
push(@$ansi_part, $color + $ext_code);
} else {
push(@$ansi_part, "$base8_code;5;$color");
}
# It's a full rgb code
} elsif ($color =~ /^#/) {
my ($rgbr, $rgbg, $rgbb) = $color =~ /.(..)(..)(..)/;
push(@$ansi_part, "$base8_code;2;" . hex($rgbr) . ";" . hex($rgbg) . ";" . hex($rgbb));
# It's a simple 16 color OG ansi
} elsif ($color ne "normal") {
my $color_num = $colors->{$color} + $base_code;
push(@$ansi_part, $color_num);
}
}
# Is the string 100% numeric
sub is_numeric {
my $s = shift();
if ($s =~ /^\d+$/) {
return 1;
}
return 0;
}
# Does the string start with ANSI
sub starts_with_ansi {
my $str = shift();
# NOTE: This is not `ansi_color_regex`, which includes "no ANSI sequences".
if ($str =~ /^$ansi_regex/) {
return 1;
} else {
return 0;
}
}
sub get_terminal_width {
# Make width static so we only calculate it once
state $width;
if ($width) {
return $width;
}
# If there is a ruler width in the config we use that
if ($ruler_width) {
$width = $ruler_width;
# Otherwise we check the terminal width using tput
} else {
my $tput = `tput cols`;
if ($tput) {
$width = int($tput);
if (is_windows()) {
$width--;
}
} else {
print color('orange') . "Warning: `tput cols` did not return numeric input" . color('reset') . "\n";
$width = 80;
}
}
return $width;
}
sub show_debug_info {
my @less = get_less_charset();
my $git_ver = trim(`git --version 2>&1`);
$git_ver =~ s/[^\d.]//g;
if ($git_ver !~ /git/) {
$git_ver = "Unknown";
} else {
$git_ver = "v" . $git_ver;
}
my $out .= "Diff-so-fancy : v$VERSION\n";
$out .= "Git : $git_ver\n";
$out .= "Perl : $^V\n";
$out .= "\n";
$out .= "Terminal width : " . get_terminal_width() . "\n";
$out .= "Terminal \$LANG : " . ($ENV{LANG} || "") . "\n";
$out .= "\n";
$out .= "Supports Unicode: " . yes_no(should_print_unicode()) . "\n";
$out .= "Unicode Ruler : " . yes_no($use_unicode_dash_for_ruler) . "\n";
$out .= "\n";
$out .= "Less Charset Var: " . ($less[0] // "") . "\n";
$out .= "Less Charset : " . ($less[1] // "") . "\n";
$out .= "\n";
$out .= "Is Windows : " . yes_no(is_windows()) . "\n";
$out .= "Operating System: $^O\n";
print $out;
my @lines = split(/\n/, $out);
debug_log(@lines);
}
# Boolean to yes/no string
sub yes_no {
my $val = shift();
if ($val) {
return "Yes";
} else {
return "No";
}
}
# If there are colors set in the gitconfig use those, otherwise leave the defaults
sub init_diff_highlight_colors {
$DiffHighlight::NEW_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.newnormal')) || $DiffHighlight::NEW_HIGHLIGHT[0];
$DiffHighlight::NEW_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.newhighlight')) || $DiffHighlight::NEW_HIGHLIGHT[1];
$DiffHighlight::NEW_HIGHLIGHT[2] = git_ansi_color(git_config('color.diff-highlight.newreset')) || "\e[0m";
$DiffHighlight::OLD_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.oldnormal')) || $DiffHighlight::OLD_HIGHLIGHT[0];
$DiffHighlight::OLD_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.oldhighlight')) || $DiffHighlight::OLD_HIGHLIGHT[1];
$DiffHighlight::NEW_HIGHLIGHT[2] = git_ansi_color(git_config('color.diff-highlight.oldreset')) || "\e[0m";
}
# Human readable datetime string with milliseconds
sub date_str {
use Time::HiRes qw(gettimeofday);
my ($sec, $usec) = gettimeofday();
my @t = localtime($sec);
my $ret = sprintf(
"%04d-%02d-%02d %02d:%02d:%02d.%03d",
$t[5] + 1900,
$t[4] + 1,
$t[3],
$t[2],
$t[1],
$t[0],
int($usec / 1000),
);
return $ret;
}
# Write a line to the debug log which is opened on the fly as-needed
# Usage: debug_log($str) or debug_log(@lines)
sub debug_log {
my @lines = @_;
my $file = "/tmp/diff-so-fancy.debug.log";
my $date_str = date_str();
state $fh;
if (!$fh) {
printf("%sDebug log enabled:%s $file\n", color('orange'), color());
open ($fh, ">", $file) or die("Cannot write to $file");
}
foreach my $log_line (@lines) {
$log_line = trim($log_line);
print $fh "$date_str: $log_line\n";
}
return 1;
}
# Find the commom prefix chars between two strings
# common_prefix("foobar", "food is yummy"); # 'foo'
sub common_prefix {
my ($a, $b) = @_;
my $len = length($a) < length($b) ? length($a) : length($b);
my $i = 0;
$i++ while $i < $len && substr($a,$i,1) eq substr($b,$i,1);
return substr($a, 0, $i);
}
# Count the number of context lines in the diff
sub calculate_context_lines {
my @lines = @_;
my $count = 0;
my $hunk_line = 0;
# Count the number of lines between the hunk line and the
# first + or - line
foreach my $line (@lines) {
# Look for the hunk line before we start
if ($line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) {
$hunk_line = $count;
} elsif ($hunk_line && $line =~ /^${ansi_color_regex}[+-]/) {
my $diff = $count - $hunk_line - 1;
#print "Hunk: $hunk_line, ChangedLine: $count ($diff context)\n";
return $diff;
}
$count++;
};
# If for some reason we can't figure it out, assume 3
return 3;
}
# This function divides Git/Diff strings into smaller chunks that d-s-f can
# process. The special sauce is not splitting on the Git headers, or any of
# the lines that require context awareness of the next two lines
sub get_diff_chunks {
my ($line_ref) = @_;
my @lines = @$line_ref;
my @end_lines = (); # Array of line numbers of the END of each chunk
my $total_lines = scalar(@lines);
# Loop through all the lines and build end line numbers for each "chunk"
for (my $i = 0; $i < $total_lines; $i++) {
my $line = $lines[$i];
if (($i > 0) && $line =~ /^${ansi_color_regex}diff/) {
push(@end_lines, $i - 1);
}
}
# The final end point is the last line
my $last_idx = $end_lines[-1] || 0;
if ($last_idx != ($total_lines - 1)) {
push(@end_lines, $total_lines - 1);
}
# Now that we know where all the end points are we divide the big array
# into smaller chunks
my @ret = ();
my $prev = 0;
foreach my $idx (@end_lines) {
if ($idx > 0) {
# Array splice the lines into a smaller chuck and put them in @ret
my @tmp = @lines[$prev .. $idx];
push(@ret, \@tmp);
$prev = $idx + 1;
}
}
return @ret;
}
# Log each chunk of diff for debugging purposes
sub log_chunks {
my $line_ref = shift();
debug_log("-------------- Chunk --------------");
debug_log(@$line_ref);
}
# In patch mode we have to keep the line ratio 1:1 so we need to know how many
# header lines there are. Usually three or four.
sub count_git_header_lines {
my @lines = @_;
my $ret = 0;
# If we're not in --patch mode then bail out early
if (!$args->{patch} && !$ENV{DSF_PATCH_MODE}) {
return 0;
}
# Textual diff should be four lines of header
#diff --git a/diff-so-fancy b/diff-so-fancy
#index d952098..424d8a3 100755
#--- a/diff-so-fancy
#+++ b/diff-so-fancy
# Binary diff should be three lines of header
#diff --git a/file.txt b/file.txt
#new file mode 100644
#index 0000000..e69de29
#diff --git a/hello.txt b/hello.txt
#new file mode 100644
#index 0000000..0c767bc
#--- /dev/null
#+++ b/hello.txt
my $count = 1;
foreach my $line (@lines) {
if ($line =~ /^${ansi_color_regex}index/) {
last;
}
$count++;
}
if ($count == 2) {
$count = 4;
}
$ret = $count;
# No Git/Diff header should be more than four lines
if ($ret > 4) {
$ret = 0;
}
return $ret;
}
# Borrowed from: https://www.perturb.org/display/1097_Perl_detect_if_a_module_is_installed_before_using_it.html
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
if (!defined(&trim)) {
*trim = sub {
my ($s) = (@_, $_); # Passed in var, or default to $_
if (length($s) == 0) { return ""; }
$s =~ s/^\s*//;
$s =~ s/\s*$//;
return $s;
}
}
if ($ENV{USER} eq "bakers") {
require Dump::Krumo;
Dump::Krumo->import(qw(k kd));
}
}
################################################################################
=pod
=encoding utf8
=head1 NAME
diff-so-fancy makes your diffs human readable instead of machine readable. This
helps improve code quality and helps you spot defects faster.
=head1 USAGE
=head2 Git
Configure git to use C<diff-so-fancy> for all diff output:
git config --global core.pager "diff-so-fancy | less --tabs=4 -RF"
git config --global interactive.diffFilter "diff-so-fancy --patch"
=head2 Diff
Use C<-u> with diff for unified output, and pipe the output to C<diff-so-fancy>:
diff -u file_a file_b | diff-so-fancy
We also support recursive mode with C<-r> or C<--recursive>
diff --recursive -u /path/folder_a /path/folder_b | diff-so-fancy
=head1 OPTIONS
B<markEmptyLines:> Colorize the first block of an empty line. (Default: true)
git config --bool --global diff-so-fancy.markEmptyLines false
B<changeHunkIndicators:> Simplify Git header chunks to a human readable format. (Default: true)
git config --bool --global diff-so-fancy.changeHunkIndicators false
B<stripLeadingSymbols:> Should the + or - symbols at line-start be removed. (Default: true)
git config --bool --global diff-so-fancy.stripLeadingSymbols false
B<useUnicodeRuler:> By default, the separator for the file header uses Unicode line-drawing
characters. If this is causing output errors on your terminal, set this to false to use ASCII
characters instead. (Default: true)
git config --bool --global diff-so-fancy.useUnicodeRuler false
B<rulerWidth:> By default, the separator for the file header spans the full width of the
terminal. Use rulerWidth to set the width of the file header manually.
git config --global diff-so-fancy.rulerWidth 80
=head1 HOMEPAGE
- https://github.com/so-fancy/diff-so-fancy
=head1 SEE ALSO
=head2 Delta
- https://github.com/dandavison/delta
=head2 Lazygit with diff-so-fancy integration
- https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff-so-fancy
# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4
================================================
FILE: diff-so-fancy.plugin.zsh
================================================
# Add our plugin diretory to user's path
#
# See following web page for explanation of the line "ZERO=...":
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
0="${${(M)0:#/*}:-$PWD/$0}"
local diff_so_fancy_bin="${0:h}"
if [[ -z "${path[(r)${diff_so_fancy_bin}]}" ]]; then
path+=( "${diff_so_fancy_bin}" )
fi
================================================
FILE: docs/diff-so-fancy.1
================================================
.\" -*- mode: troff; coding: utf-8 -*-
.\" Automatically generated by Pod::Man 5.0102 (Pod::Simple 3.45)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>.
.ie n \{\
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds C`
. ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
. if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. if !\nF==2 \{\
. nr % 0
. nr F 2
. \}
. \}
.\}
.rr rF
.\" ========================================================================
.\"
.IX Title "DIFF-SO-FANCY 1"
.TH DIFF-SO-FANCY 1 2026-03-18 "perl v5.40.2" "User Contributed Perl Documentation"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
.nh
.SH NAME
diff\-so\-fancy makes your diffs human readable instead of machine readable. This
helps improve code quality and helps you spot defects faster.
.SH USAGE
.IX Header "USAGE"
.SS Git
.IX Subsection "Git"
Configure git to use \f(CW\*(C`diff\-so\-fancy\*(C'\fR for all diff output:
.PP
.Vb 2
\& git config \-\-global core.pager "diff\-so\-fancy | less \-\-tabs=4 \-RF"
\& git config \-\-global interactive.diffFilter "diff\-so\-fancy \-\-patch"
.Ve
.SS Diff
.IX Subsection "Diff"
Use \f(CW\*(C`\-u\*(C'\fR with diff for unified output, and pipe the output to \f(CW\*(C`diff\-so\-fancy\*(C'\fR:
.PP
.Vb 1
\& diff \-u file_a file_b | diff\-so\-fancy
.Ve
.PP
We also support recursive mode with \f(CW\*(C`\-r\*(C'\fR or \f(CW\*(C`\-\-recursive\*(C'\fR
.PP
.Vb 1
\& diff \-\-recursive \-u /path/folder_a /path/folder_b | diff\-so\-fancy
.Ve
.SH OPTIONS
.IX Header "OPTIONS"
\&\fBmarkEmptyLines:\fR Colorize the first block of an empty line. (Default: true)
.PP
.Vb 1
\& git config \-\-bool \-\-global diff\-so\-fancy.markEmptyLines false
.Ve
.PP
\&\fBchangeHunkIndicators:\fR Simplify Git header chunks to a human readable format. (Default: true)
.PP
.Vb 1
\& git config \-\-bool \-\-global diff\-so\-fancy.changeHunkIndicators false
.Ve
.PP
\&\fBstripLeadingSymbols:\fR Should the + or \- symbols at line-start be removed. (Default: true)
.PP
.Vb 1
\& git config \-\-bool \-\-global diff\-so\-fancy.stripLeadingSymbols false
.Ve
.PP
\&\fBuseUnicodeRuler:\fR By default, the separator for the file header uses Unicode line-drawing
characters. If this is causing output errors on your terminal, set this to false to use ASCII
characters instead. (Default: true)
.PP
.Vb 1
\& git config \-\-bool \-\-global diff\-so\-fancy.useUnicodeRuler false
.Ve
.PP
\&\fBrulerWidth:\fR By default, the separator for the file header spans the full width of the
terminal. Use rulerWidth to set the width of the file header manually.
.PP
.Vb 1
\& git config \-\-global diff\-so\-fancy.rulerWidth 80
.Ve
.SH HOMEPAGE
.IX Header "HOMEPAGE"
\&\- https://github.com/so\-fancy/diff\-so\-fancy
.SH "SEE ALSO"
.IX Header "SEE ALSO"
.SS Delta
.IX Subsection "Delta"
\&\- https://github.com/dandavison/delta
.SS "Lazygit with diff-so-fancy integration"
.IX Subsection "Lazygit with diff-so-fancy integration"
\&\- https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md#diff\-so\-fancy
.PP
# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4
================================================
FILE: hacking-and-testing.md
================================================
### Hacking
```sh
# fork and clone the diff-so-fancy repo.
git clone https://github.com/so-fancy/diff-so-fancy/ && cd diff-so-fancy
# test a saved diff against your local version
cat test/fixtures/ls-function.diff | ./diff-so-fancy
# setup symlinks to use local copy
npm link
cd ~/projects/catfabulator && git diff
```
### Running tests
The tests use [bats-core](https://bats-core.readthedocs.io/en/latest/index.html), the Bash automated testing system.
```sh
# initialize the bats components
git submodule sync && git submodule update --init
# run the test suite once:
./test/bats/bin/bats test
# run it on every change with `entr`
brew install entr
find ./* test/* test/fixtures/* -maxdepth 0 | entr ./test/bats/bin/bats test
```
When writing assertions, you'll likely want to compare to expected output. To grab that reliably, you can use something like `git --no-pager diff | ./diff-so-fancy > output.txt`
### Publishing to npm
Run these one-by-one, manually.
```sh
diff-so-fancy --version # see the old version (probably)
npm run build # build the fatpack into dist.
./dist/diff-so-fancy --version # get latest version from perl script
npm version vX.X.X # bump package.json to match.
npm uninstall -g diff-so-fancy && npm link # make global symlink, if not already present
diff-so-fancy --version # ensure latest version is shown
git show | diff-so-fancy # ensure it works
npm publish --dry-run # ensure listed files are what you want published. update .npmignore as desired
# npm login --registry https://registry.npmjs.org/ # maybe.
npm publish --registry https://registry.npmjs.org/
```
================================================
FILE: history.md
================================================
## History
`diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13349456), which grew into a [standalone script](https://github.com/paulirish/dotfiles/blob/63cb8193b0e66cf80ab6332477f1f52c7fbb9311/bin/diff-so-fancy). Later, [@stevemao](https://github.com/stevemao) brought it into its [own repo](https://github.com/so-fancy/diff-so-fancy) (here), and gave it the room to mature. It's quickly grown into a [widely collaborative project](https://github.com/so-fancy/diff-so-fancy/graphs/contributors).
================================================
FILE: lib/DiffHighlight.pm
================================================
package DiffHighlight;
use 5.008;
use warnings FATAL => 'all';
use strict;
# Use the correct value for both UNIX and Windows (/dev/null vs nul)
use File::Spec;
my $NULL = File::Spec->devnull();
# Highlight by reversing foreground and background. You could do
# other things like bold or underline if you prefer.
our @OLD_HIGHLIGHT = (
undef,
"\e[7m",
"\e[27m",
);
our @NEW_HIGHLIGHT = (
$OLD_HIGHLIGHT[0],
$OLD_HIGHLIGHT[1],
$OLD_HIGHLIGHT[2],
);
my $RESET = "\x1b[m";
my $COLOR = qr/\x1b\[[0-9;]*m/;
my $BORING = qr/$COLOR|\s/;
my @removed;
my @added;
my $in_hunk;
my $graph_indent = 0;
our $line_cb = sub { print @_ };
our $flush_cb = sub { local $| = 1 };
# Count the visible width of a string, excluding any terminal color sequences.
sub visible_width {
local $_ = shift;
my $ret = 0;
while (length) {
if (s/^$COLOR//) {
# skip colors
} elsif (s/^.//) {
$ret++;
}
}
return $ret;
}
# Return a substring of $str, omitting $len visible characters from the
# beginning, where terminal color sequences do not count as visible.
sub visible_substr {
my ($str, $len) = @_;
while ($len > 0) {
if ($str =~ s/^$COLOR//) {
next
}
$str =~ s/^.//;
$len--;
}
return $str;
}
sub handle_line {
my $orig = shift;
local $_ = $orig;
# match a graph line that begins a commit
if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space
$COLOR?\*$COLOR?[ ] # a "*" with its trailing space
(?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|"
[ ]* # trailing whitespace for merges
/x) {
my $graph_prefix = $&;
# We must flush before setting graph indent, since the
# new commit may be indented differently from what we
# queued.
flush();
$graph_indent = visible_width($graph_prefix);
} elsif ($graph_indent) {
if (length($_) < $graph_indent) {
$graph_indent = 0;
} else {
$_ = visible_substr($_, $graph_indent);
}
}
if (!$in_hunk) {
$line_cb->($orig);
$in_hunk = /^$COLOR*\@\@ /;
}
elsif (/^$COLOR*-/) {
push @removed, $orig;
}
elsif (/^$COLOR*\+/) {
push @added, $orig;
}
else {
flush();
$line_cb->($orig);
$in_hunk = /^$COLOR*[\@ ]/;
}
# Most of the time there is enough output to keep things streaming,
# but for something like "git log -Sfoo", you can get one early
# commit and then many seconds of nothing. We want to show
# that one commit as soon as possible.
#
# Since we can receive arbitrary input, there's no optimal
# place to flush. Flushing on a blank line is a heuristic that
# happens to match git-log output.
if (/^$/) {
$flush_cb->();
}
}
sub flush {
# Flush any queued hunk (this can happen when there is no trailing
# context in the final diff of the input).
show_hunk(\@removed, \@added);
@removed = ();
@added = ();
}
sub highlight_stdin {
while (<STDIN>) {
handle_line($_);
}
flush();
}
# Ideally we would feed the default as a human-readable color to
# git-config as the fallback value. But diff-highlight does
# not otherwise depend on git at all, and there are reports
# of it being used in other settings. Let's handle our own
# fallback, which means we will work even if git can't be run.
sub color_config {
my ($key, $default) = @_;
my $s = `git config --get-color $key 2>$NULL`;
return length($s) ? $s : $default;
}
sub show_hunk {
my ($a, $b) = @_;
# If one side is empty, then there is nothing to compare or highlight.
if (!@$a || !@$b) {
$line_cb->(@$a, @$b);
return;
}
# If we have mismatched numbers of lines on each side, we could try to
# be clever and match up similar lines. But for now we are simple and
# stupid, and only handle multi-line hunks that remove and add the same
# number of lines.
if (@$a != @$b) {
$line_cb->(@$a, @$b);
return;
}
my @queue;
for (my $i = 0; $i < @$a; $i++) {
my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
$line_cb->($rm);
push @queue, $add;
}
$line_cb->(@queue);
}
sub highlight_pair {
my @a = split_line(shift);
my @b = split_line(shift);
# Find common prefix, taking care to skip any ansi
# color codes.
my $seen_plusminus;
my ($pa, $pb) = (0, 0);
while ($pa < @a && $pb < @b) {
if ($a[$pa] =~ /$COLOR/) {
$pa++;
}
elsif ($b[$pb] =~ /$COLOR/) {
$pb++;
}
elsif ($a[$pa] eq $b[$pb]) {
$pa++;
$pb++;
}
elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
$seen_plusminus = 1;
$pa++;
$pb++;
}
else {
last;
}
}
# Find common suffix, ignoring colors.
my ($sa, $sb) = ($#a, $#b);
while ($sa >= $pa && $sb >= $pb) {
if ($a[$sa] =~ /$COLOR/) {
$sa--;
}
elsif ($b[$sb] =~ /$COLOR/) {
$sb--;
}
elsif ($a[$sa] eq $b[$sb]) {
$sa--;
$sb--;
}
else {
last;
}
}
if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT),
highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT);
}
else {
return join('', @a),
join('', @b);
}
}
# we split either by $COLOR or by character. This has the side effect of
# leaving in graph cruft. It works because the graph cruft does not contain "-"
# or "+"
sub split_line {
local $_ = shift;
return utf8::decode($_) ?
map { utf8::encode($_); $_ }
map { /$COLOR/ ? $_ : (split //) }
split /($COLOR+)/ :
map { /$COLOR/ ? $_ : (split //) }
split /($COLOR+)/;
}
sub highlight_line {
my ($line, $prefix, $suffix, $theme) = @_;
my $start = join('', @{$line}[0..($prefix-1)]);
my $mid = join('', @{$line}[$prefix..$suffix]);
my $end = join('', @{$line}[($suffix+1)..$#$line]);
# If we have a "normal" color specified, then take over the whole line.
# Otherwise, we try to just manipulate the highlighted bits.
if (defined $theme->[0]) {
s/$COLOR//g for ($start, $mid, $end);
chomp $end;
chomp $start;
return join('',
$theme->[0], $start, $RESET,
$theme->[1], $mid, $RESET,
$theme->[0], $end, $RESET,
"\n"
);
} else {
return join('',
$start,
$theme->[1], $mid, $theme->[2],
$end
);
}
}
# Pairs are interesting to highlight only if we are going to end up
# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
# is just useless noise. We can detect this by finding either a matching prefix
# or suffix (disregarding boring bits like whitespace and colorization).
sub is_pair_interesting {
my ($a, $pa, $sa, $b, $pb, $sb) = @_;
my $prefix_a = join('', @$a[0..($pa-1)]);
my $prefix_b = join('', @$b[0..($pb-1)]);
my $suffix_a = join('', @$a[($sa+1)..$#$a]);
my $suffix_b = join('', @$b[($sb+1)..$#$b]);
return visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ ||
visible_substr($prefix_b, $graph_indent) !~ /^$COLOR*\+$BORING*$/ ||
$suffix_a !~ /^$BORING*$/ ||
$suffix_b !~ /^$BORING*$/;
}
================================================
FILE: package.json
================================================
{
"name": "diff-so-fancy",
"version": "1.4.6",
"description": "Good-lookin' diffs with diff-highlight and more",
"bin": {
"diff-so-fancy": "dist/diff-so-fancy"
},
"files": [
"dist/diff-so-fancy"
],
"scripts": {
"test": "./test/bats/bin/bats test",
"build": "mkdir -p dist && PATH=\"$HOME/perl5/bin:$PATH\" PERL5LIB=\"$HOME/perl5/lib/perl5:$PERL5LIB\" perl ./third_party/build_fatpack/build.pl --output ./dist/diff-so-fancy"
},
"repository": {
"type": "git",
"url": "https://github.com/so-fancy/diff-so-fancy"
},
"keywords": [
"git",
"diff",
"fancy",
"good-lookin'",
"diff-highlight",
"color",
"readable",
"highlight"
],
"author": "Paul Irish",
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"bugs": {
"url": "https://github.com/so-fancy/diff-so-fancy/issues"
},
"homepage": "https://github.com/so-fancy/diff-so-fancy#readme"
}
================================================
FILE: pro-tips.md
================================================
# Pro-tips
## One-off fanciness or a specific diff-so-fancy alias
You can do also do a one-off:
```shell
git diff --color | diff-so-fancy
```
or configure an alias and a corresponding pager to use `diff-so-fancy`:
```shell
git config --global alias.dsf "diff --color"
git config --global pager.dsf "diff-so-fancy | less --tabs=4 -RFXS"
```
## Opting-out
Sometimes you will want to bypass diff-so-fancy. Use `--no-pager` for that:
```shell
git --no-pager diff
```
## Raw patches
As a shortcut for a 'normal' diff to save as a patch for emailing or later
application, it may be helpful to configure an alias:
```ini
[alias]
patch = !git --no-pager diff --no-color
```
which can then be used as `git patch > changes.patch`.
## Improved colors for the highlighted bits
The default Git colors are not optimal. The colors used for the screenshot were:
```shell
git config --global color.ui true
git config --global color.diff-highlight.oldNormal "red bold"
git config --global color.diff-highlight.oldHighlight "red bold 52"
git config --global color.diff-highlight.newNormal "green bold"
git config --global color.diff-highlight.newHighlight "green bold 22"
git config --global color.diff.meta "11"
git config --global color.diff.frag "magenta bold"
git config --global color.diff.func "146 bold"
git config --global color.diff.commit "yellow bold"
git config --global color.diff.old "red bold"
git config --global color.diff.new "green bold"
git config --global color.diff.whitespace "red reverse"
```
#### Moving around in the diff
You can pre-seed your `less` pager with a search pattern so that you can move
between files with `n`/`N` keys:
```ini
[pager]
diff = diff-so-fancy | less --tabs=4 -RFXS --pattern '^(Date|added|deleted|modified): '
```
## Zsh plugin suppport for diff-so-fancy
This project includes a `.plugin.zsh` file providing ZSH framework support, so you can use any framework that supports the ZSH plugin standard to install `diff-so-fancy` and add it to your `$PATH`. Installation with Zinit, Zplug, and Zgen:
### Install with zinit
```sh
zinit ice lucid as"program" pick"bin/git-dsf"
zinit load so-fancy/diff-so-fancy
```
### Install with zplug
```sh
zplug "so-fancy/diff-so-fancy", as:command, use:bin/git-dsf
```
### zgenom and others
```sh
zgenom load so-fancy/diff-so-fancy
```
## `hg` configuration
You can configure `hg diff` output to use `diff-so-fancy` by adding this alias
to your `hgrc` file:
```ini
[alias]
diff = !HGPLAIN=1 $HG diff --pager=on --config pager.pager=diff-so-fancy $@
```
================================================
FILE: report-bug.sh
================================================
#!/bin/bash
clipboard() {
local copy_cmd
if [ -n "$PBCOPY_SERVER" ]; then
local body="" # buffer
body=$(cat)
# while IFS= read -r buffer; do
# body="$body$buffer\n";
# done
curl "$PBCOPY_SERVER" --data-urlencode body="$body" >/dev/null 2>&1
return $?
fi
if type putclip >/dev/null 2>&1; then
copy_cmd="putclip"
elif [ -e /dev/clipboard ];then
cat > /dev/clipboard
return 0
elif type clip >/dev/null 2>&1; then
if [[ $LANG = UTF-8 ]]; then
copy_cmd="iconv -f utf-8 -t shift_jis | clip"
else
copy_cmd=clip
fi
# copy_cmd=clip
elif command -v pbcopy >/dev/null 2>&1; then
copy_cmd="pbcopy"
elif command -v xclip >/dev/null 2>&1; then
# copy_cmd="xclip -i -selection clipboard"
copy_cmd="xclip"
elif command -v xsel >/dev/null 2>&1 ; then
local copy_cmd="xsel -b"
fi
if [ -n "$copy_cmd" ] ;then
eval "$copy_cmd"
else
echo "clipboard is unavailable" 1>&2
fi
}
if [ $# -eq 0 ]; then
echo "Usage: $0 'git diff HEAD..HEAD^'"
exit 7
fi
file=${2:-dsf-bug-report.txt}
{
echo "$1"
eval "$1"
echo ""
echo ""
echo ""
echo "$1 --color"
eval "$1 --color"
echo "git config --list | grep pager"
eval "git config --list | grep pager"
} | base64 > "$file"
echo "Wrote file: $file"
echo "Please open a new issue on Github and attach it"
================================================
FILE: reporting-bugs.md
================================================
### Reporting bugs
If you find a bug using the following command
```sh
git diff HEAD..HEAD^
```
You can use [report-bug.sh](./report-bug.sh) we provide
```sh
./report-bug.sh 'git diff HEAD..HEAD^'
```
Attach the output file to the GitHub issue you create.
================================================
FILE: test/bugs.bats
================================================
#!/usr/bin/env bats
# Used by both `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load 'test_helper/util'
}
setup_file() {
__load_imports__
setup_default_dsf_git_config
}
setup() {
__load_imports__
}
teardown_file() {
teardown_default_dsf_git_config
}
# https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13459061
@test "All removed lines are present in diff" {
output=$( load_fixture "chromium-modaltoelement" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 7 --partial "WebInspector.Dialog"
assert_line --index 7 --partial "5;52m" # red oldhighlight
assert_line --index 8 --partial "WebInspector.Dialog"
assert_line --index 8 --partial "5;22m" # green newhighlight
}
@test "File with space in the name (#360)" {
output=$( load_fixture "file_with_space" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --regexp "added:.*a b"
}
@test "Vanilla diff with add/remove empty lines (#366)" {
output=$( load_fixture "add_remove_empty_lines" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 5 --partial "5;22m" # green added line
assert_line --index 8 --partial "5;52m" # red removed line
}
@test "recursive vanilla diff -r -bu as Mercurial (#436)" {
output=$( load_fixture "recursive_default_as_mercurial" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --partial "renamed:"
assert_line --index 3 --partial "@ language/app.py:1 @"
assert_line --index 19 --partial "renamed:"
assert_line --index 21 --partial "@ language/__init__.py:1 @"
assert_line --index 25 --partial "renamed:"
assert_line --index 27 --partial "@ language/README.md:1 @"
}
@test "recursive vanilla diff --recursive -u as Mercurial (#436)" {
output=$( load_fixture "recursive_longhand_as_mercurial" | $diff_so_fancy )
run printf "%s" "$output"
assert_output --regexp 'modified: app.py'
assert_output --regexp 'modified: __init__.py'
assert_output --regexp 'modified: README.md'
}
@test "Functional part with bright color (#444)" {
output=$( load_fixture "move_with_content_change" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 3 --partial "@[0m[93m height"
}
@test "ANSI Reset without the zero (#469)" {
output=$( load_fixture "ansi_reset_no_number" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 5 --partial "History"
}
@test "File copy detection (#349)" {
output=$( load_fixture "file_copy" | $diff_so_fancy )
run printf "%s" "$output"
assert_output --regexp 'Copied first_file to copied_file'
}
@test "diff --recursive support (#394)" {
output=$( load_fixture "diff_recursive" | $diff_so_fancy )
run printf "%s" "$output"
assert_output --regexp 'modified: foo/bar'
assert_output --regexp 'modified: index.txt'
}
@test "Remove a \n at the end of a file (#474)" {
output=$( load_fixture "remove_slashn_eof" | $diff_so_fancy )
echo $output
run printf "%s" "$output"
#assert_output --regexp 'one'
assert_line --index 6 --regexp "three"
assert_line --index 7 --regexp "three"
}
================================================
FILE: test/diff-so-fancy.bats
================================================
#!/usr/bin/env bats
# Helper invoked by `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load 'test_helper/util'
}
setup_file() {
__load_imports__
set_env
setup_default_dsf_git_config
# bats fails to handle our multiline result, so we save to $output ourselves
__dsf_cached_output="$( load_fixture "ls-function" | $diff_so_fancy )"
export __dsf_cached_output
}
setup() {
__load_imports__
output="${__dsf_cached_output}"
}
teardown_file() {
teardown_default_dsf_git_config
}
@test "diff-so-fancy runs and exits without error" {
load_fixture "ls-function" | $diff_so_fancy
run assert_success
}
@test "index line is removed entirely" {
refute_output --partial "index 33c3d8b..fd54db2 100644"
}
@test "+/- line symbols are stripped" {
run printf "%s" "$output"
refute_line --index 9 --regexp "\+ set -x CLICOLOR_FORCE 1"
refute_line --index 22 --regexp "- eval \"env CLICOLOR"
}
@test "+/- line symbols are stripped (truecolor)" {
output=$( load_fixture "truecolor" | $diff_so_fancy )
refute_output --partial "
[1;38;2;220;50;47;48;2;0;43;54m-"
refute_output --partial "
[1;38;2;133;153;0;48;2;0;43;54m+"
}
@test "empty lines added/removed are marked" {
run printf "%s" "$output"
assert_line --index 7 --partial "[7m[1;32m [m"
assert_line --index 24 --partial "[7m[1;31m [m"
#assert_output --partial "[7m[1;32m [m"
#assert_output --partial "[7m[1;31m [m"
}
@test "diff --git line is removed entirely" {
# test against ls-function
refute_output --partial "diff --git a/fish/functions/ls.fish"
# test with git config diff.noprefix true
output=$( load_fixture "noprefix" | $diff_so_fancy )
refute_output --partial "diff --git setup-a-new-machine.sh"
}
@test "header format uses a native line-drawing character" {
header=$( load_fixture "ls-function" | $diff_so_fancy | head -n8 )
run printf "%s" "$header"
assert_line --index 0 --partial "─────"
assert_line --index 1 --partial "modified: fish/functions/ls.fish"
assert_line --index 2 --partial "─────"
}
# see https://git.io/vrOF4
@test "Should not show unicode bytes in hex if missing LC_*/LANG _and_ piping the output" {
unset LESSCHARSET LESSCHARDEF LC_ALL LC_CTYPE LANG
# pipe to cat(1) so we don't open stdout
header=$( printf "%s" "$(load_fixture "ls-function" | $diff_so_fancy | cat)" | head -n8 )
run printf "%s" "$header"
assert_line --index 0 --partial "-----"
assert_line --index 1 --partial "modified: fish/functions/ls.fish"
assert_line --index 2 --partial "-----"
set_env # reset env
}
@test "Leading dashes are not handled as modified" {
output=$( load_fixture "leading-dashes" | $diff_so_fancy )
refute_output --partial "modified: Callback"
}
@test "Handle binary modifications" {
output=$( load_fixture "binary-modified" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --partial "modified: cancel.png (binary)";
}
@test "Handle unicode characters in diff output" {
output=$( load_fixture "unicode" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 5 --partial "åäöç"
}
@test "Handle latin1 encoding sanely" {
output=$( load_fixture "latin1" | $diff_so_fancy )
# Make sure the output contains SOME of the english text (i.e. it doesn't barf on the whole line)
run printf "%s" "$output"
assert_line --index 6 --partial "saw he conqu"
}
@test "Correctly handle hunk definition with no comma" {
output=$( load_fixture "hunk_no_comma" | $diff_so_fancy )
# On single line removes there is NO comma in the hunk,
# make sure the first column is still correctly stripped.
run printf "%s" "$output"
assert_line --index 5 --regexp "after"
}
@test "Empty file add" {
output=$( load_fixture "add_empty_file" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 5 --regexp "added:.*empty_file.txt"
}
@test "Empty file delete" {
output=$( load_fixture "remove_empty_file" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 5 --regexp "deleted:.*empty_file.txt"
}
@test "Move with content change" {
output=$( load_fixture "move_with_content_change" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --regexp "renamed:"
}
@test "Mercurial support" {
output=$( load_fixture "hg" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --regexp "modified: hello.c"
}
@test "Handle file renames" {
output=$( load_fixture "file-rename" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --partial "renamed:"
assert_line --index 1 --partial "Changes.new"
assert_line --index 1 --partial "bin/"
}
@test "header_clean 'added:'" {
output=$( load_fixture "file-moves" | $diff_so_fancy )
assert_output --regexp 'added:.*hello.txt'
}
@test "header_clean 'modified:'" {
output=$( load_fixture "file-moves" | $diff_so_fancy )
assert_output --partial 'modified: appveyor.yml'
}
@test "header_clean 'deleted:'" {
output=$( load_fixture "file-moves" | $diff_so_fancy )
assert_output --regexp 'deleted:.*circle.yml'
}
@test "header_clean permission changes" {
output=$( load_fixture "file-perms" | $diff_so_fancy )
assert_output --partial 'circle.yml changed file mode from 100644 to 100755'
}
@test "header_clean 'new file mode' is removed" {
output=$( load_fixture "file-perms" | $diff_so_fancy )
refute_output --partial 'new file mode'
}
@test "header_clean 'deleted file mode' is removed" {
output=$( load_fixture "file-perms" | $diff_so_fancy )
refute_output --partial 'deleted file mode'
}
@test "header_clean remove 'git --diff' header" {
output=$( load_fixture "file-perms" | $diff_so_fancy )
refute_output --partial 'diff --git'
}
@test "Reworked hunks" {
output=$( load_fixture "file-moves" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 46 --partial "@ package.json:1 @"
assert_line --index 79 --partial "@ square.yml:1 @"
}
@test "Reworked hunks (noprefix)" {
output=$( load_fixture "noprefix" | $diff_so_fancy )
assert_output --partial '@ setup-a-new-machine.sh:33 @'
assert_output --partial '@ setup-a-new-machine.sh:219 @'
}
@test "Reworked hunks (deleted files)" {
output=$( load_fixture "dotfiles" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 188 --partial "@ bin/diff-so-fancy:1 @"
}
@test "Hunk formatting: @@@ -A,B -C,D +E,F @@@" {
# stderr forced into output
output=$( load_fixture "complex-hunks" | $diff_so_fancy 2>&1 )
run printf "%s" "$output"
assert_output --regexp "@ libs/header_clean/header_clean.pl:107 @"
refute_output --partial 'Use of uninitialized value'
}
@test "Hunk formatting: @@ -1,6 +1,6 @@" {
# stderr forced into output
output=$( load_fixture "first-three-line" | $diff_so_fancy )
assert_output --partial '@ package.json:3 @'
}
@test "Hunk formatting: @@ -1 0,0 @@" {
# stderr forced into output
output=$( load_fixture "single-line-remove" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 4 --regexp 'var delayedMessage = "It worked";'
}
@test "Three way merge" {
# stderr forced into output
output=$( load_fixture "complex-hunks" | $diff_so_fancy )
# Lines should not have + or - in at the start
refute_output --partial '- my $foo = shift(); # Array passed in by reference'
refute_output --partial '+ my $array = shift(); # Array passed in by reference'
refute_output --partial ' sub parse_hunk_header {'
}
@test "mnemonicprefix handling" {
output=$( load_fixture "mnemonicprefix" | $diff_so_fancy )
assert_output --partial 'modified: test/header_clean.bats'
}
@test "non-git diff parsing" {
output=$( load_fixture "weird" | $diff_so_fancy )
run printf "%s" "$output"
assert_line --index 1 --partial "modified: doc/manual.xml.head"
assert_line --index 3 --partial "@ doc/manual.xml.head:8355 @"
}
================================================
FILE: test/fixtures/add_empty_file.diff
================================================
commit ef0e63dbd7e8df6cde4ee5599ad65db7820888ef
Author: Scott Baker <fake@domain.com>
Date: Wed Jul 19 08:00:11 2017 -0700
Add empty file
diff --git a/empty_file.txt b/empty_file.txt
new file mode 100644
index 0000000..e69de29
================================================
FILE: test/fixtures/add_remove_empty_lines.diff
================================================
--- one.txt 2020-04-23 10:15:29.193452325 -0700
+++ two.txt 2020-04-23 10:15:37.634463619 -0700
@@ -1,5 +1,5 @@
1
+
2
3
-
4
================================================
FILE: test/fixtures/ansi_reset_no_number.diff
================================================
[93mdiff --git a/history.md b/history.md[m
[93mindex f6776e0..a6b4546 100644[m
[93m--- a/history.md[m
[93m+++ b/history.md[m
[1;35m@@ -1,3 +1,3 @@[m
[1;31m-[m[1;31m## History[m
[1;32m+[m[1;32m## Historyz[m
[m
[m`diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6743b907ff586c28cd36e08d1e1c634e2968893e#commitcomment-13349456), which grew into a [standalone script](https://github.com/paulirish/dotfiles/blob/63cb8193b0e66cf80ab6332477f1f52c7fbb9311/bin/diff-so-fancy). Later, [@stevemao](https://github.com/stevemao) brought it into its [own repo](https://github.com/so-fancy/diff-so-fancy) (here), and gave it the room to mature. It's quickly grown into a [widely collaborative project](https://github.com/so-fancy/diff-so-fancy/graphs/contributors).[m
================================================
FILE: test/fixtures/binary-modified.diff
================================================
[1;33mdiff --git a/cancel.png b/cancel.png[m
[1;33mindex 667e10c..06fc49c 100644[m
Binary files a/cancel.png and b/cancel.png differ
[1;33mdiff --git a/diff-so-fancy b/diff-so-fancy[m
[1;33mindex 0f2911c..5811717 100755[m
[1;33m--- a/diff-so-fancy[m
[1;33m+++ b/diff-so-fancy[m
[1;35m@@ -32,13 +32,6 @@[m [mcolor_code_regex="(${CSI}([0-9]{1,3}(;[0-9]{1,3}){0,3})[m|K])?"[m
[m
git_index_line_pattern="^($color_code_regex)index .*"[m
[m
[1;31m-format_diff_header () {[m
[1;31m- # simplify the unified patch diff header[m
[1;31m- $SED -E "/$git_index_line_pattern/{N;s/$git_index_line_pattern\n//;}" \[m
[1;31m- | $SED -E "s/^($color_code_regex)\-\-\-(.*)$/\1$(print_horizontal_rule)\\${NL}\1---\5/g" \[m
[1;31m- | $SED -E "s/^($color_code_regex)\+\+\+(.*)$/\1+++\5\\${NL}\1$(print_horizontal_rule)/g"[m
[1;31m-}[m
[1;31m-[m
print_horizontal_rule () {[m
let width="$(tput cols)"[m
[m
================================================
FILE: test/fixtures/chromium-modaltoelement.diff
================================================
[1;33mdiff --git a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js[m
[1;33mindex 4f9adf8..8c13743 100644[m
[1;33m--- a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js[m
[1;33m+++ b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js[m
[1;35m@@ -32,7 +32,7 @@[m
* @constructor[m
* @extends {WebInspector.Widget}[m
*/[m
[1;31m-WebInspector.Dialog = function()[m
[1;32m+[m[1;32mWebInspector.Dialog = function(isModalToElement)[m
{[m
WebInspector.Widget.call(this, true);[m
this.markAsRoot();[m
[1;35m@@ -45,6 +45,9 @@[m [mWebInspector.Dialog = function()[m
[m
this._wrapsContent = false;[m
this._dimmed = false;[m
[1;32m+[m[1;32m this._isModalToElement = isModalToElement;[m
[1;32m+[m
[1;32m+[m[1;32m this._glassPane = new WebInspector.GlassPane(relativeToElement, isModalToElement);[m
/** @type {!Map<!HTMLElement, number>} */[m
this._tabIndexMap = new Map();[m
}[m
[1;35m@@ -62,16 +65,16 @@[m [mWebInspector.Dialog.prototype = {[m
/**[m
* @override[m
*/[m
[1;31m- show: function()[m
[1;32m+[m[1;32m show: function(isModalToElement)[m
{[m
if (WebInspector.Dialog._instance)[m
WebInspector.Dialog._instance.detach();[m
WebInspector.Dialog._instance = this;[m
[m
[1;31m- var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument);[m
[1;32m+[m[1;32m var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument, isModalToElement);[m
this._disableTabIndexOnElements(document);[m
[m
[1;31m- this._glassPane = new WebInspector.GlassPane(document, this._dimmed);[m
[1;32m+[m[1;32m this._glassPane = new WebInspector.GlassPane(document, isModalToElement);[m
this._glassPane.element.addEventListener("click", this._onGlassPaneClick.bind(this), false);[m
WebInspector.GlassPane.DefaultFocusedViewStack.push(this);[m
[m
================================================
FILE: test/fixtures/complex-hunks.diff
================================================
[1;32mcommit 74804e377d4a54d1173d4393827d4e4b27e4d5d0[m
[1;33mdiff --cc libs/header_clean/header_clean.pl[m
[1;33mindex e8bcd92,5970580..ae279d0[m
[1;33m--- a/libs/header_clean/header_clean.pl[m
[1;33m+++ b/libs/header_clean/header_clean.pl[m
[1;35m@@@ -105,13 -104,21 +104,23 @@@[m [mfor (my $i = 0; $i <= $#input; $i++) [m
}[m
}[m
[m
[1;32m+ # Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805[m
[1;32m+ sub parse_hunk_header {[m
[1;32m+ my ($line) = @_;[m
[1;32m+ my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =[m
[1;32m+ $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;[m
[1;32m+ $o_cnt = 1 unless defined $o_cnt;[m
[1;32m+ $n_cnt = 1 unless defined $n_cnt;[m
[1;32m+ return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);[m
[1;32m+ }[m
[1;32m+ [m
sub strip_empty_first_line {[m
[1;31m - my $foo = shift(); # Array passed in by reference[m
[1;32m + my $array = shift(); # Array passed in by reference[m
[m
# If the first line is just whitespace remove it[m
[1;31m - if (defined($foo->[0]) && $foo->[0] =~ /^\s*$/) {[m
[1;31m - shift($foo);[m
[1;32m + if (defined($array->[0]) && $array->[0] =~ /^\s*$/) {[m
[1;32m + shift(@$array); # Throw away the first line[m
}[m
[1;32m +[m
[1;32m + return 1;[m
}[m
================================================
FILE: test/fixtures/diff_recursive.diff
================================================
Only in /var/tmp/b: donk
diff --recursive -u /var/tmp/a/foo/bar /var/tmp/b/foo/bar
--- /var/tmp/a/foo/bar 2026-03-13 21:35:28.492228513 -0700
+++ /var/tmp/b/foo/bar 2026-03-13 21:35:47.101220189 -0700
@@ -1 +1 @@
-BAR
+FOO
diff --recursive -u /var/tmp/a/index.txt /var/tmp/b/index.txt
--- /var/tmp/a/index.txt 2026-03-13 21:35:20.997231861 -0700
+++ /var/tmp/b/index.txt 2026-03-13 21:35:41.030222906 -0700
@@ -1 +1,2 @@
INDEX
+INDEX2
================================================
FILE: test/fixtures/dotfiles.diff
================================================
[1;33mdiff --git a/.aliases b/.aliases[m
[1;33mindex 30182ef..be9fb1d 100644[m
[1;33m--- a/.aliases[m
[1;33m+++ b/.aliases[m
[1;35m@@ -18,6 +18,7 @@[m [malias brwe=brew #typos[m
[m
alias hosts='sudo $EDITOR /etc/hosts' # yes I occasionally 127.0.0.1 twitter.com ;)[m
[m
[1;32m+[m[1;32malias ag='ag -W 200 -f'[m
[m
###[m
# time to upgrade `ls`[m
[1;35m@@ -51,7 +52,7 @@[m [malias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup[m
[m
# Networking. IP address, dig, DNS[m
alias ip="dig +short myip.opendns.com @resolver1.opendns.com"[m
[1;31m-alias dig="dig +nocmd any +multiline +noall +answer"[m
[1;32m+[m[1;32m# alias dig="dig +nocmd any +multiline +noall +answer"[m
[m
# Recursively delete `.DS_Store` files[m
alias cleanup_dsstore="find . -name '*.DS_Store' -type f -ls -delete"[m
[1;35m@@ -68,7 +69,13 @@[m [malias ungz="gunzip -k"[m
alias fs="stat -f \"%z bytes\""[m
[m
# Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage[m
[1;31m-alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage"[m
[1;32m+[m[1;32malias emptytrash=" \[m[7;31m [m
[1;32m+[m[1;32m sudo rm -rfv /Volumes/*/.Trashes; \[m
[1;32m+[m[1;32m rm -rfv ~/.Trash/*; \[m
[1;32m+[m[1;32m sudo rm -v /private/var/vm/sleepimage; \[m
[1;32m+[m[1;32m rm -rv \"/Users/paulirish/Library/Application Support/stremio/Cache\"; \[m
[1;32m+[m[1;32m rm -rv \"/Users/paulirish/Library/Application Support/stremio/stremio-cache\" \[m
[1;32m+[m[1;32m"[m
[m
# Update installed Ruby gems, Homebrew, npm, and their installed packages[m
alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor"[m
[1;33mdiff --git a/.bash_profile b/.bash_profile[m
[1;33mindex 8f17751..def367d 100644[m
[1;33m--- a/.bash_profile[m
[1;33m+++ b/.bash_profile[m
[1;35m@@ -116,3 +116,9 @@[m [mshopt -s cdspell;[m
[m
[m
[m
[1;32m+[m
[1;32m+[m[1;32m# The next line updates PATH for the Google Cloud SDK.[m
[1;32m+[m[1;32msource '/Users/paulirish/google-cloud-sdk/path.bash.inc'[m
[1;32m+[m
[1;32m+[m[1;32m# The next line enables shell command completion for gcloud.[m
[1;32m+[m[1;32msource '/Users/paulirish/google-cloud-sdk/completion.bash.inc'[m
[1;33mdiff --git a/.bash_prompt b/.bash_prompt[m
[1;33mindex 852d69f..8d3e3d0 100644[m
[1;33m--- a/.bash_prompt[m
[1;33m+++ b/.bash_prompt[m
[1;35m@@ -4,6 +4,9 @@[m
default_username='paulirish'[m
[m
[m
[1;32m+[m[1;32meval "$(thefuck --alias)"[m
[1;32m+[m
[1;32m+[m
if [[ -n "$ZSH_VERSION" ]]; then # quit now if in zsh[m
return 1 2> /dev/null || exit 1;[m
fi;[m
[1;33mdiff --git a/.bashrc b/.bashrc[m
[1;33mindex 877b68f..18461ac 100644[m
[1;33m--- a/.bashrc[m
[1;33m+++ b/.bashrc[m
[1;35m@@ -1,2 +1,4 @@[m
[ -n "$PS1" ] && source ~/.bash_profile[m
[m
[1;32m+[m
[1;32m+[m[1;32m# [ -f ~/.fzf.bash ] && source ~/.fzf.bash[m
[1;33mdiff --git a/.functions b/.functions[m
[1;33mindex 8292d2e..a548d45 100644[m
[1;33m--- a/.functions[m
[1;33m+++ b/.functions[m
[1;35m@@ -28,6 +28,19 @@[m [mcdf() { # short for cdfinder[m
}[m
[m
[m
[1;32m+[m
[1;32m+[m[1;32m# git commit browser. needs fzf[m
[1;32m+[m[1;32mlog() {[m
[1;32m+[m[1;32m git log --graph --color=always \[m
[1;32m+[m[1;32m --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" |[m
[1;32m+[m[1;32m fzf --ansi --no-sort --reverse --tiebreak=index --toggle-sort=\` \[m
[1;32m+[m[1;32m --bind "ctrl-m:execute:[m
[1;32m+[m[1;32m echo '{}' | grep -o '[a-f0-9]\{7\}' | head -1 |[m
[1;32m+[m[1;32m xargs -I % sh -c 'git show --color=always % | less -R'"[m
[1;32m+[m[1;32m}[m
[1;32m+[m
[1;32m+[m
[1;32m+[m
# Start an HTTP server from a directory, optionally specifying the port[m
function server() {[m
local port="${1:-8000}"[m
[1;35m@@ -145,17 +158,14 @@[m [mwebmify(){[m
ffmpeg -i $1 -vcodec libvpx -acodec libvorbis -isync -copyts -aq 80 -threads 3 -qmax 30 -y $2 $1.webm[m
}[m
[m
[1;32m+[m[1;32m# direct it all to /dev/null[m
[1;32m+[m[1;32mfunction nullify() {[m
[1;32m+[m[1;32m "$@" >/dev/null 2>&1[m
[1;32m+[m[1;32m}[m
[1;32m+[m
[m
# visual studio code. a la `subl`[m
[1;31m-code () {[m
[1;31m- if [[ $# = 0 ]][m
[1;31m- then[m
[1;31m- open -a "Visual Studio Code"[m
[1;31m- else[m
[1;31m- [[ $1 = /* ]] && F="$1" || F="$PWD/${1#./}"[m
[1;31m- open -a "Visual Studio Code" --args "$F"[m
[1;31m- fi[m
[1;31m-}[m
[1;32m+[m[1;32mfunction code () { VSCODE_CWD="$PWD" open -n -b "com.microsoft.VSCode" --args $*; }[m
[m
# `shellswitch [bash |zsh]`[m
# Must be in /etc/shells[m
[1;33mdiff --git a/.gitconfig b/.gitconfig[m
[1;33mindex d2be05f..d32f98c 100644[m
[1;33m--- a/.gitconfig[m
[1;33m+++ b/.gitconfig[m
[1;35m@@ -7,11 +7,13 @@[m
df = diff --color --color-words --abbrev[m
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --[m
co = checkout[m
[1;32m+[m [1;32mcherry = cherry-pick # does this conflict with the real `git cherry`?[m
[m
# Show the diff between the latest commit and the current state[m
d = !"git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat"[m
[m
[1;31m- reup = rebase-update[m
[1;32m+[m [1;32m# chromium/depot_tools convenience. manual[m
[1;32m+[m [1;32mreup = rebase-update --no_fetch --keep-going[m
[m
[m
[m
[1;35m@@ -23,7 +25,7 @@[m
excludesfile = ~/.gitignore[m
attributesfile = ~/.gitattributes[m
# insanely beautiful diffs[m
[1;31m- pager = diff-highlight | diff-so-fancy | less -r[m
[1;32m+[m [1;32mpager = diff-so-fancy | less --tabs=1,5 -R[m
[color "branch"][m
current = yellow reverse[m
local = yellow[m
[1;35m@@ -40,7 +42,8 @@[m
changed = green[m
untracked = cyan[m
[merge][m
[1;31m- tool = opendiff[m
[1;32m+[m [1;32m#tool = opendiff[m
[1;32m+[m [1;32mtool = kdiff3[m
[m
[m
[color "diff-highlight"][m
[1;35m@@ -86,3 +89,5 @@[m
required = true[m
[init][m
templatedir = ~/.git_template[m
[1;32m+[m[1;32m[http][m
[1;32m+[m [1;32mcookiefile = /Users/paulirish/.gitcookies[m
[1;33mdiff --git a/.zshrc b/.zshrc[m
[1;33mindex 410a2ca..dbad355 100644[m
[1;33m--- a/.zshrc[m
[1;33m+++ b/.zshrc[m
[1;35m@@ -113,3 +113,5 @@[m [msource ~/.bash_profile[m
[m
[m
export PATH="$PATH:$HOME/.rvm/bin" # Add RVM to PATH for scripting[m
[1;32m+[m
[1;32m+[m[1;32m[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh[m
[1;33mdiff --git a/README.md b/README.md[m
[1;33mindex 496bbbb..171eaea 100644[m
[1;33m--- a/README.md[m
[1;33m+++ b/README.md[m
[1;35m@@ -55,7 +55,6 @@[m [mLastly, I use `open .` to open Finder from this path. (That's just available nor[m
## overview of files[m
[m
#### Automatic config[m
[1;31m-* `.sift.conf` - sift (faster than grep, ack, ag)[m
* `.vimrc`, `.vim` - vim config, obv.[m
* `.inputrc` - behavior of the actual prompt line[m
[m
[1;33mdiff --git a/bin/diff-highlight b/bin/diff-highlight[m
[1;33mdeleted file mode 120000[m
[1;33mindex 7c5c827..0000000[m
[1;33m--- a/bin/diff-highlight[m
[1;33m+++ /dev/null[m
[1;35m@@ -1 +0,0 @@[m
[1;31m-/Users/paulirish/.homebrew/share/git-core/contrib/diff-highlight/diff-highlight[m
\ No newline at end of file[m
[1;33mdiff --git a/bin/diff-so-fancy b/bin/diff-so-fancy[m
[1;33mdeleted file mode 100755[m
[1;33mindex 5b004a2..0000000[m
[1;33m--- a/bin/diff-so-fancy[m
[1;33m+++ /dev/null[m
[1;35m@@ -1,69 +0,0 @@[m
[1;31m-#!/bin/bash[m
[1;31m-[m
[1;31m-###############[m
[1;31m-# diff-so-fancy builds on the good-lookin' output of diff-highlight to upgrade your diffs' appearances[m
[1;31m-# * Output will not be in standard patch format, but will be readable[m
[1;31m-# * No pesky `+` or `-` at line-stars, making for easier copy-paste.[m
[1;31m-#[m
[1;31m-# Screenshot: https://github.com/paulirish/dotfiles/commit/6743b907ff58#commitcomment-13349456[m
[1;31m-#[m
[1;31m-#[m
[1;31m-# Usage[m
[1;31m-#[m
[1;31m-# git diff | diff-highlight | diff-so-fancy[m
[1;31m-#[m
[1;31m-# Add to .gitconfig so all `git diff` uses it.[m
[1;31m-# git config --global core.pager "diff-highlight | diff-so-fancy | less -r"[m
[1;31m-#[m
[1;31m-#[m
[1;31m-# Requirements / Install[m
[1;31m-#[m
[1;31m-# * GNU sed. On Mac, install it with Homebrew:[m
[1;31m-# brew install gnu-sed --default-names # You'll have to change below to `gsed` otherwise[m
[1;31m-# * diff-highlight. It's shipped with Git, but probably not in your $PATH[m
[1;31m-# ln -sf "$(brew --prefix)/share/git-core/contrib/diff-highlight/diff-highlight" ~/bin/diff-highlight[m
[1;31m-# * Add some coloring to your .gitconfig:[m
[1;31m-# git config --global color.diff-highlight.oldNormal "red bold"[m
[1;31m-# git config --global color.diff-highlight.oldHighlight "red bold 52"[m
[1;31m-# git config --global color.diff-highlight.newNormal "green bold"[m
[1;31m-# git config --global color.diff-highlight.newHighlight "green bold 22"[m
[1;31m-#[m
[1;31m-###############[m
[1;31m-[m
[1;31m-# TODO:[m
[1;31m-# Put on NPM.[m
[1;31m-[m
[1;31m-[m
[1;31m-[ $# -ge 1 -a -f "$1" ] && input="$1" || input="-"[m
[1;31m-[m
[1;31m-color_code_regex=$'(\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)[m|K])?'[m
[1;31m-reset_color="\x1B\[m"[m
[1;31m-dim_magenta="\x1B\[38;05;146m"[m
[1;31m-[m
[1;31m-format_diff_header () {[m
[1;31m- # simplify the unified patch diff header[m
[1;31m- sed -E "s/^($color_code_regex)diff --git .*$//g" | \[m
[1;31m- sed -E "s/^($color_code_regex)index .*$/\[m
[1;31m-\1$(print_horizontal_rule)/g" | \[m
[1;31m- sed -E "s/^($color_code_regex)\+\+\+(.*)$/\1\+\+\+\5\\[m
[1;31m-\1$(print_horizontal_rule)/g"[m
[1;31m-}[m
[1;31m-[m
[1;31m-colorize_context_line () {[m
[1;31m- # extra color for @@ context line[m
[1;31m- sed -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g"[m
[1;31m-}[m
[1;31m-[m
[1;31m-strip_leading_symbols () {[m
[1;31m- # strip the + and -[m
[1;31m- sed -E "s/^($color_code_regex)[\+\-]/\1 /g"[m
[1;31m-}[m
[1;31m-[m
[1;31m-print_horizontal_rule () {[m
[1;31m- printf "%$(tput cols)s\n"|tr " " "─"[m
[1;31m-}[m
[1;31m-[m
[1;31m-# run it.[m
[1;31m-cat $input | format_diff_header | colorize_context_line | strip_leading_symbols[m
[1;31m-[m
[1;31m-[m
[1;33mdiff --git a/brew-cask.sh b/brew-cask.sh[m
[1;33mindex 24c2ba5..3f3c02a 100755[m
[1;33m--- a/brew-cask.sh[m
[1;33m+++ b/brew-cask.sh[m
[1;35m@@ -1,8 +1,8 @@[m
#!/bin/bash[m
[m
[m
[1;31m-# to maintain cask .... [m
[1;31m-# brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup` [m
[1;32m+[m[1;32m# to maintain cask ....[m
[1;32m+[m[1;32m# brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup`[m
[m
[m
# Install native apps[m
[1;35m@@ -14,7 +14,7 @@[m [mbrew tap caskroom/versions[m
brew cask install spectacle[m
brew cask install dropbox[m
brew cask install gyazo[m
[1;31m-brew cask install onepassword[m
[1;32m+[m[1;32mbrew cask install 1password[m
brew cask install rescuetime[m
brew cask install flux[m
[m
[1;35m@@ -47,6 +47,6 @@[m [mbrew cask install utorrent[m
[m
# Not on cask but I want regardless.[m
[m
[1;31m-# 3Hub https://itunes.apple.com/us/app/3hub/id427515976?mt=12 [m
[1;32m+[m[1;32m# 3Hub https://itunes.apple.com/us/app/3hub/id427515976?mt=12[m
# File Multi Tool 5[m
# Phosphor[m
\ No newline at end of file[m
[1;33mdiff --git a/brew.sh b/brew.sh[m
[1;33mindex 8715a37..c4a663e 100755[m
[1;33m--- a/brew.sh[m
[1;33m+++ b/brew.sh[m
[1;35m@@ -58,7 +58,9 @@[m [mbrew install mtr[m
[m
[m
# Install other useful binaries[m
[1;31m-brew install sift[m
[1;32m+[m[1;32mbrew install the_silver_searcher[m
[1;32m+[m[1;32mbrew install fzf[m
[1;32m+[m
brew install git[m
brew install imagemagick --with-webp[m
brew install node # This installs `npm` too using the recommended installation method[m
[1;33mdiff --git a/fish/aliases.fish b/fish/aliases.fish[m
[1;33mindex a0fe9b3..fc990f6 100644[m
[1;33m--- a/fish/aliases.fish[m
[1;33m+++ b/fish/aliases.fish[m
[1;35m@@ -26,17 +26,14 @@[m [malias brwe=brew #typos[m
alias hosts='sudo $EDITOR /etc/hosts' # yes I occasionally 127.0.0.1 twitter.com ;)[m
[m
[m
[1;31m-# `shellswitch [bash|zsh|fish]`[m
[1;31m-function shellswitch[m
[1;31m- chsh -s (brew --prefix)/bin/$argv[m
[1;31m-end[m
[1;31m-[m
[1;31m-[m
[1;31m-[m
# `cat` with beautiful colors. requires Pygments installed.[m
# sudo easy_install -U Pygments[m
alias c='pygmentize -O style=monokai -f console256 -g'[m
[m
[1;32m+[m[1;32malias ag='ag -W 200 -f'[m
[1;32m+[m
[1;32m+[m[1;32malias diskspace_report="df -P -kHl"[m
[1;32m+[m[1;32malias free_diskspace_report="diskspace_report"[m
[m
[m
# Networking. IP address, dig, DNS[m
[1;35m@@ -54,8 +51,7 @@[m [malias ungz="gunzip -k"[m
# File size[m
alias fs="stat -f \"%z bytes\""[m
[m
[1;31m-# Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage[m
[1;31m-alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage"[m
[1;32m+[m[1;32m# emptytrash written as a function[m
[m
# Update installed Ruby gems, Homebrew, npm, and their installed packages[m
alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor"[m
[1;33mdiff --git a/fish/config.fish b/fish/config.fish[m
[1;33mindex 9c4bd70..fd19027 100755[m
[1;33m--- a/fish/config.fish[m
[1;33m+++ b/fish/config.fish[m
[1;35m@@ -2,9 +2,15 @@[m [mset default_user "paulirish"[m
set default_machine "paulirish-macbookair2"[m
[m
[m
[1;32m+[m[1;32m#set -x DYLD_FALLBACK_LIBRARY_PATH /Users/paulirish/.homebrew/lib[m
[1;32m+[m
source ~/.config/fish/path.fish[m
source ~/.config/fish/aliases.fish[m
source ~/.config/fish/chpwd.fish[m
[1;32m+[m[1;32msource ~/.config/fish/functions.fish[m
[1;32m+[m
[1;32m+[m
[1;32m+[m
[m
[m
# Completions[m
[1;35m@@ -22,8 +28,6 @@[m [mend[m
make_completion g 'git'[m
[m
[m
[1;31m-[m
[1;31m-[m
# Readline colors[m
set -g fish_color_autosuggestion 555 yellow[m
set -g fish_color_command 5f87d7[m
[1;35m@@ -83,3 +87,4 @@[m [mset -gx LESS_TERMCAP_so \e'[38;5;246m' # begin standout-mode - info box[m
set -gx LESS_TERMCAP_ue \e'[0m' # end underline[m
set -gx LESS_TERMCAP_us \e'[04;38;5;146m' # begin underline[m
[m
[7;31m+[m
[1;33mdiff --git a/fish/functions/fish_prompt.fish b/fish/functions/fish_prompt.fish[m
[1;33mindex 04abe4b..96bfa3e 100755[m
[1;33m--- a/fish/functions/fish_prompt.fish[m
[1;33m+++ b/fish/functions/fish_prompt.fish[m
[1;35m@@ -13,6 +13,9 @@[m [mfunction _git_current_branch -d "Output git's current branch name"[m
end ^/dev/null | sed -e 's|^refs/heads/||'[m
end[m
[m
[1;32m+[m[1;32mfunction fish_title --description 'Show current path (abbreviated) in iTerm tab title '[m
[1;32m+[m[1;32m echo (prompt_pwd)[m
[1;32m+[m[1;32mend[m
[m
function fish_prompt --description 'Write out the prompt'[m
[m
[1;33mdiff --git a/fish/functions/gr.fish b/fish/functions/gr.fish[m
[1;33mindex 4c2722d..1a0ee1b 100644[m
[1;33m--- a/fish/functions/gr.fish[m
[1;33m+++ b/fish/functions/gr.fish[m
[1;35m@@ -1,3 +1,12 @@[m
[m
# git root[m
[1;31m-alias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup || pwd`'[m
[1;32m+[m[1;32mfunction gr --description "Jump to the git root"[m
[1;32m+[m [1;32mset -l repo_info (command git rev-parse --git-dir --is-inside-git-dir --is-bare-repository --is-inside-work-tree --short HEAD ^/dev/null)[m
[1;32m+[m[7;31m [m [1;32mtest -n "$repo_info"; or return[m
[1;32m+[m
[1;32m+[m [1;32mset -l cd_up_path (command git rev-parse --show-cdup)[m
[1;32m+[m
[1;32m+[m [1;32mif test -n $cd_up_path[m
[1;32m+[m [1;32mcd $cd_up_path[m
[1;32m+[m [1;32mend[m
[1;32m+[m[1;32mend[m
[1;33mdiff --git a/fish/functions/ls.fish b/fish/functions/ls.fish[m
[1;33mindex 33c3d8b..fd54db2 100644[m
[1;33m--- a/fish/functions/ls.fish[m
[1;33m+++ b/fish/functions/ls.fish[m
[1;35m@@ -7,6 +7,8 @@[m
if begin[m
type gls 1>/dev/null 2>/dev/null[m
or command ls --version 1>/dev/null 2>/dev/null[m
[1;32m+[m
[1;32m+[m[1;32m set -x CLICOLOR_FORCE 1[m
end[m
# This is GNU ls[m
function ls --description "List contents of directory"[m
[1;35m@@ -22,11 +24,11 @@[m [mif begin[m
set param $param --human-readable[m
set param $param --sort=extension[m
set param $param --group-directories-first[m
[1;31m- if isatty 1[m
[1;32m+[m[1;32m if isatty 1[m
set param $param --indicator-style=classify[m
end[m
[m
[1;31m- eval "env CLICOLOR_FORCE=1 command $ls $param $argv"[m
[1;32m+[m[1;32m eval $ls $param "$argv"[m
end[m
[m
if not set -q LS_COLORS[m
[1;33mdiff --git a/fish/path.fish b/fish/path.fish[m
[1;33mindex 6e28fad..aa9d014 100644[m
[1;33m--- a/fish/path.fish[m
[1;33m+++ b/fish/path.fish[m
[1;35m@@ -13,8 +13,10 @@[m [mfor entry in (string split \n $PATH_DIRS)[m
# resolve the {$HOME} substitutions[m
set -l resolved_path (eval echo $entry)[m
if test -d "$resolved_path"; # and not contains $resolved_path $PATH[m
[1;31m- set -x PA $PA "$resolved_path"[m
[1;32m+[m[1;32m set PA $PA "$resolved_path"[m
end[m
end[m
[m
[1;31m-set -x PATH $PA[m
\ No newline at end of file[m
[1;32m+[m[1;32mset PA $PA /Users/paulirish/.rvm/gems/ruby-2.2.1/bin[m
[1;32m+[m
[1;32m+[m[1;32mset --export PATH $PA[m
\ No newline at end of file[m
[1;33mdiff --git a/setup-a-new-machine.sh b/setup-a-new-machine.sh[m
[1;33mindex 7b5996c..7e60889 100755[m
[1;33m--- a/setup-a-new-machine.sh[m
[1;33m+++ b/setup-a-new-machine.sh[m
[1;35m@@ -215,5 +215,7 @@[m [msh .osx[m
# symlink it up![m
./symlink-setup.sh[m
[m
[1;32m+[m[1;32m# add manual symlink for .ssh/config and probably .config/fish[m
[1;32m+[m
###[m
##############################################################################################################[m
================================================
FILE: test/fixtures/file-moves.diff
================================================
[33mcommit a12f64cfa2388b1d07659149db3a77314c9836c8[m
Author: Scott Baker <bakers@canbytel.com>
Date: Thu Feb 25 08:31:54 2016 -0800
Moves
[38;5;11mdiff --git a/appveyor.yml b/appveyor.yml[m
[38;5;11mindex a6fb95e..43d61c3 100644[m
[38;5;11m--- a/appveyor.yml[m
[38;5;11m+++ b/appveyor.yml[m
[36m@@ -1,4 +1,4 @@[m
[38;5;9m-install:[m
[38;5;10m+[m[38;5;10mFOOinstall:[m
- git clone --depth 1 https://github.com/sstephenson/bats.git[m
[m
build: false[m
[38;5;11mdiff --git a/circle.yml b/circle.yml[m
[38;5;11mdeleted file mode 100644[m
[38;5;11mindex 18a04a3..0000000[m
[38;5;11m--- a/circle.yml[m
[38;5;11m+++ /dev/null[m
[36m@@ -1,21 +0,0 @@[m
[38;5;9m-machine:[m
[38;5;9m- environment:[m
[38;5;9m-[m
[38;5;9m-[m
[38;5;9m-dependencies:[m
[38;5;9m- pre:[m
[38;5;9m- - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";[m
[38;5;9m- - sudo apt-get -y update[m
[38;5;9m- - sudo apt-get -y install shellcheck;[m
[38;5;9m-[m
[38;5;9m- post:[m
[38;5;9m- - git clone --depth 1 https://github.com/sstephenson/bats.git[m
[38;5;9m-[m
[38;5;9m-checkout:[m
[38;5;9m- post:[m
[38;5;9m- - git submodule update --init[m
[38;5;9m-[m
[38;5;9m-test:[m
[38;5;9m- override:[m
[38;5;9m- - bats/bin/bats test/*.bats[m
[38;5;9m- - shellcheck diff-so-fancy update-deps.sh[m
[38;5;11mdiff --git a/hello.txt b/hello.txt[m
[38;5;11mnew file mode 100644[m
[38;5;11mindex 0000000..0c767bc[m
[38;5;11m--- /dev/null[m
[38;5;11m+++ b/hello.txt[m
[36m@@ -0,0 +1 @@[m
[38;5;10m+[m[38;5;10mHI THERE[m
[38;5;11mdiff --git a/package.json b/package.json[m
[38;5;11mdeleted file mode 100644[m
[38;5;11mindex c7a1ab2..0000000[m
[38;5;11m--- a/package.json[m
[38;5;11m+++ /dev/null[m
[36m@@ -1,29 +0,0 @@[m
[38;5;9m-{[m
[38;5;9m- "name": "diff-so-fancy",[m
[38;5;9m- "version": "0.5.0",[m
[38;5;9m- "description": "Good-lookin' diffs with diff-highlight and more",[m
[38;5;9m- "bin": {[m
[38;5;9m- "diff-so-fancy": "diff-so-fancy",[m
[38;5;9m- "diff-highlight": "third_party/diff-highlight/diff-highlight"[m
[38;5;9m- },[m
[38;5;9m- "repository": {[m
[38;5;9m- "type": "git",[m
[38;5;9m- "url": "git+https://github.com/so-fancy/diff-so-fancy.git"[m
[38;5;9m- },[m
[38;5;9m- "keywords": [[m
[38;5;9m- "git",[m
[38;5;9m- "diff",[m
[38;5;9m- "fancy",[m
[38;5;9m- "good-lookin'",[m
[38;5;9m- "diff-highlight",[m
[38;5;9m- "color",[m
[38;5;9m- "readable",[m
[38;5;9m- "highlight"[m
[38;5;9m- ],[m
[38;5;9m- "author": "Paul Irish",[m
[38;5;9m- "license": "MIT",[m
[38;5;9m- "bugs": {[m
[38;5;9m- "url": "https://github.com/so-fancy/diff-so-fancy/issues"[m
[38;5;9m- },[m
[38;5;9m- "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"[m
[38;5;9m-}[m
[38;5;11mdiff --git a/square.yml b/square.yml[m
[38;5;11mnew file mode 100644[m
[38;5;11mindex 0000000..18a04a3[m
[38;5;11m--- /dev/null[m
[38;5;11m+++ b/square.yml[m
[36m@@ -0,0 +1,21 @@[m
[38;5;10m+[m[38;5;10mmachine:[m
[38;5;10m+[m[38;5;10m environment:[m
[38;5;10m+[m
[38;5;10m+[m
[38;5;10m+[m[38;5;10mdependencies:[m
[38;5;10m+[m[38;5;10m pre:[m
[38;5;10m+[m[38;5;10m - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe";[m
[38;5;10m+[m[38;5;10m - sudo apt-get -y update[m
[38;5;10m+[m[38;5;10m - sudo apt-get -y install shellcheck;[m
[38;5;10m+[m
[38;5;10m+[m[38;5;10m post:[m
[38;5;10m+[m[38;5;10m - git clone --depth 1 https://github.com/sstephenson/bats.git[m
[38;5;10m+[m
[38;5;10m+[m[38;5;10mcheckout:[m
[38;5;10m+[m[38;5;10m post:[m
[38;5;10m+[m[38;5;10m - git submodule update --init[m
[38;5;10m+[m
[38;5;10m+[m[38;5;10mtest:[m
[38;5;10m+[m[38;5;10m override:[m
[38;5;10m+[m[38;5;10m - bats/bin/bats test/*.bats[m
[38;5;10m+[m[38;5;10m - shellcheck diff-so-fancy update-deps.sh[m
================================================
FILE: test/fixtures/file-perms.diff
================================================
[38;5;227mdiff --git a/circle.yml b/circle.yml[m
[38;5;227mold mode 100644[m
[38;5;227mnew mode 100755[m
[38;5;227mdiff --git a/foo.json b/foo.json[m
[38;5;227mnew file mode 100644[m
[38;5;227mindex 0000000..316a815[m
[38;5;227m--- /dev/null[m
[38;5;227m+++ b/foo.json[m
[1;35m@@ -0,0 +1,29 @@[m
[38;5;10m+[m[38;5;10m{[m
[38;5;10m+[m[38;5;10m "name": "diff-so-fancy",[m
[38;5;10m+[m[38;5;10m "version": "0.5.1",[m
[38;5;10m+[m[38;5;10m "description": "Good-lookin' diffs with diff-highlight and more",[m
[38;5;10m+[m[38;5;10m "bin": {[m
[38;5;10m+[m[38;5;10m "diff-so-fancy": "diff-so-fancy",[m
[38;5;10m+[m[38;5;10m "diff-highlight": "third_party/diff-highlight/diff-highlight"[m
[38;5;10m+[m[38;5;10m },[m
[38;5;10m+[m[38;5;10m "repository": {[m
[38;5;10m+[m[38;5;10m "type": "git",[m
[38;5;10m+[m[38;5;10m "url": "git+https://github.com/so-fancy/diff-so-fancy.git"[m
[38;5;10m+[m[38;5;10m },[m
[38;5;10m+[m[38;5;10m "keywords": [[m
[38;5;10m+[m[38;5;10m "git",[m
[38;5;10m+[m[38;5;10m "diff",[m
[38;5;10m+[m[38;5;10m "fancy",[m
[38;5;10m+[m[38;5;10m "good-lookin'",[m
[38;5;10m+[m[38;5;10m "diff-highlight",[m
[38;5;10m+[m[38;5;10m "color",[m
[38;5;10m+[m[38;5;10m "readable",[m
[38;5;10m+[m[38;5;10m "highlight"[m
[38;5;10m+[m[38;5;10m ],[m
[38;5;10m+[m[38;5;10m "author": "Paul Irish",[m
[38;5;10m+[m[38;5;10m "license": "MIT",[m
[38;5;10m+[m[38;5;10m "bugs": {[m
[38;5;10m+[m[38;5;10m "url": "https://github.com/so-fancy/diff-so-fancy/issues"[m
[38;5;10m+[m[38;5;10m },[m
[38;5;10m+[m[38;5;10m "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"[m
[38;5;10m+[m[38;5;10m}[m
[38;5;227mdiff --git a/package.json b/package.json[m
[38;5;227mdeleted file mode 100644[m
[38;5;227mindex 316a815..0000000[m
[38;5;227m--- a/package.json[m
[38;5;227m+++ /dev/null[m
[1;35m@@ -1,29 +0,0 @@[m
[38;5;9m-{[m
[38;5;9m- "name": "diff-so-fancy",[m
[38;5;9m- "version": "0.5.1",[m
[38;5;9m- "description": "Good-lookin' diffs with diff-highlight and more",[m
[38;5;9m- "bin": {[m
[38;5;9m- "diff-so-fancy": "diff-so-fancy",[m
[38;5;9m- "diff-highlight": "third_party/diff-highlight/diff-highlight"[m
[38;5;9m- },[m
[38;5;9m- "repository": {[m
[38;5;9m- "type": "git",[m
[38;5;9m- "url": "git+https://github.com/so-fancy/diff-so-fancy.git"[m
[38;5;9m- },[m
[38;5;9m- "keywords": [[m
[38;5;9m- "git",[m
[38;5;9m- "diff",[m
[38;5;9m- "fancy",[m
[38;5;9m- "good-lookin'",[m
[38;5;9m- "diff-highlight",[m
[38;5;9m- "color",[m
[38;5;9m- "readable",[m
[38;5;9m- "highlight"[m
[38;5;9m- ],[m
[38;5;9m- "author": "Paul Irish",[m
[38;5;9m- "license": "MIT",[m
[38;5;9m- "bugs": {[m
[38;5;9m- "url": "https://github.com/so-fancy/diff-so-fancy/issues"[m
[38;5;9m- },[m
[38;5;9m- "homepage": "https://github.com/so-fancy/diff-so-fancy#readme"[m
[38;5;9m-}[m
================================================
FILE: test/fixtures/file-rename.diff
================================================
diff --git a/Changes.new b/bin/Changes.new
similarity index 100%
rename from Changes.new
rename to bin/Changes.new
diff --git a/dist.ini b/dist.ini
index ca8c2ad..d2874a4 100644
--- a/dist.ini
+++ b/dist.ini
@@ -1,4 +1,4 @@
-name = csvgrep
+name = csvgrepper
author = Neil Bowers <neilb@cpan.org>
license = Perl_5
copyright_holder = Neil Bowers
@@ -9,6 +9,7 @@ version = 0.05
[@Basic]
[PkgVersion]
[AutoPrereqs]
+[BAZ]
[GithubMeta]
[MetaJSON]
================================================
FILE: test/fixtures/file_copy.diff
================================================
diff --git a/first_file b/copied_file
similarity index 100%
copy from first_file
copy to copied_file
================================================
FILE: test/fixtures/file_with_space.diff
================================================
diff --git a/a b b/a b
new file mode 100644
index 0000000..e69de29
================================================
FILE: test/fixtures/first-three-line.diff
================================================
commit fbf440dd9c32a60f9bc97693a6bd7b5ca87ec9fc
Author: Steve Mao <maochenyan@gmail.com>
Date: Wed Mar 9 19:21:27 2016 +1100
0.6.2
diff --git package.json package.json
index 7379c98..3cba6ee 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
{
"name": "diff-so-fancy",
- "version": "0.6.1",
+ "version": "0.6.2",
"description": "Good-lookin' diffs with diff-highlight and more",
"bin": {
"diff-so-fancy": "diff-so-fancy",
================================================
FILE: test/fixtures/hg.diff
================================================
diff -r 82e55d328c8c hello.c
--- a/hello.c Fri Aug 26 01:21:28 2005 -0700
+++ b/hello.c Fri Dec 29 14:37:26 2017 -0800
@@ -1,16 +1,15 @@
/*
* hello.c
*
- * Placed in the public domain by Bryan O'Sullivan
- *
* This program is not covered by patents in the United States or other
* countries.
*/
-#include <stdio.h>
+#include <fstdio.h>
int main(int argc, char **argv)
{
printf("hello, world!\n");
+ exit 99;
return 0;
}
================================================
FILE: test/fixtures/hunk_no_comma.diff
================================================
diff --git i/file w/file
index 90be1f3..294186e 100644
--- i/file
+++ w/file
@@ -1 +1 @@
-before
+after
================================================
FILE: test/fixtures/latin1.diff
================================================
[38;5;227mdiff --git a/test.txt b/test.txt[m
[38;5;227mindex 5836369..51ccf71 100644[m
[38;5;227m--- a/test.txt[m
[38;5;227m+++ b/test.txt[m
[1;35m@@ -1,6 +1 @@[m
[38;5;9m-é[m
[38;5;9m-[m
[38;5;10m+[m[38;5;10mH cam h saw he conqurd[m
================================================
FILE: test/fixtures/leading-dashes.diff
================================================
diff --git i/lib/awful/widget/keyboardlayout.lua w/lib/awful/widget/keyboardlayout.lua
index f9142b8..b6e8ce0 100644
--- i/lib/awful/widget/keyboardlayout.lua
+++ w/lib/awful/widget/keyboardlayout.lua
@@ -113,7 +113,7 @@ keyboardlayout.xkeyboard_country_code = {
["za"] = true, -- South Africa
}
--- Callback for updaing current layout
+-- Callback for updating current layout.
local function update_status (self)
self._current = awesome.xkb_get_layout_group();
local text = ""
================================================
FILE: test/fixtures/ls-function.diff
================================================
[1;33mdiff --git a/fish/functions/ls.fish b/fish/functions/ls.fish[m
[1;33mindex 33c3d8b..fd54db2 100644[m
[1;33m--- a/fish/functions/ls.fish[m
[1;33m+++ b/fish/functions/ls.fish[m
[1;35m@@ -7,6 +7,8 @@[m
if begin[m
type gls 1>/dev/null 2>/dev/null[m
or command ls --version 1>/dev/null 2>/dev/null[m
[1;32m+[m
[1;32m+[m[1;32m set -x CLICOLOR_FORCE 1[m
end[m
# This is GNU ls[m
function ls --description "List contents of directory"[m
[1;35m@@ -22,11 +24,11 @@[m [mif begin[m
set param $param --human-readable[m
set param $param --sort=extension[m
set param $param --group-directories-first[m
[1;31m- if isatty 1[m
[1;32m+[m[1;32m if isatty 1[m
set param $param --indicator-style=classify[m
end[m
[m
[1;31m- eval "env CLICOLOR_FORCE=1 command $ls $param $argv"[m
[1;32m+[m[1;32m eval $ls $param "$argv"[m
end[m
[1;31m-[m
if not set -q LS_COLORS[m
================================================
FILE: test/fixtures/mnemonicprefix.diff
================================================
[1;33mdiff --git i/diff-so-fancy w/diff-so-fancy[m
[1;33mindex 2323d9b..a280985 100755[m
[1;33m--- i/diff-so-fancy[m
[1;33m+++ w/diff-so-fancy[m
[1;35m@@ -48,10 +48,6 @@[m [mcolorize_context_line () {[m
$SED -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g"[m
}[m
[m
[1;31m-mark_empty_lines () {[m
[1;31m- $SED -E "s/^$color_code_regex[\+\-]$reset_color\s*$/$invert_color\1 $reset_escape/g"[m
[1;31m-}[m
[1;31m-[m
strip_leading_symbols () {[m
# strip the + and -[m
$SED -E "s/^($color_code_regex)[\+\-]/\1 /g"[m
[1;35m@@ -83,7 +79,6 @@[m [mcat $input \[m
| $diff_highlight \[m
| format_diff_header \[m
| colorize_context_line \[m
[1;31m- | mark_empty_lines \[m
| strip_leading_symbols \[m
| strip_first_column \[m
| print_header_clean[m
[1;33mdiff --git i/libs/header_clean/header_clean.pl w/libs/header_clean/header_clean.pl[m
[1;33mindex 23df5e7..54b3da1 100755[m
[1;33m--- i/libs/header_clean/header_clean.pl[m
[1;33m+++ w/libs/header_clean/header_clean.pl[m
[1;35m@@ -9,6 +9,8 @@[m [mmy $remove_file_delete_header = 1;[m
my $clean_permission_changes = 1;[m
my $change_hunk_indicators = 1;[m
[m
[1;32m+[m[1;32muse Data::Dump::Color;[m
[1;32m+[m
#################################################################################[m
[m
my $ansi_sequence_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,3})[mK])?/;[m
[1;35m@@ -29,12 +31,12 @@[m [mfor (my $i = 0; $i <= $#input; $i++) {[m
########################################[m
# Find the first file: --- a/README.md #[m
########################################[m
[1;31m- } elsif ($line =~ /^$ansi_sequence_regex--- (a\/)?(.+?)(\e|$)/) {[m
[1;32m+[m [1;32m} elsif ($line =~ /^$ansi_sequence_regex--- ([abiwco]\/)?(.+?)(\e|$)/) {[m
$file_1 = $5;[m
[m
# Find the second file on the next line: +++ b/README.md[m
my $next = $input[++$i];[m
[1;31m- $next =~ /^$ansi_sequence_regex\+\+\+ (b\/)?(.+?)(\e|$)/;[m
[1;32m+[m [1;32m$next =~ /^$ansi_sequence_regex\+\+\+ ([abiwco]\/)?(.+?)(\e|$)/;[m
if ($1) {[m
print $1; # Print out whatever color we're using[m
}[m
[1;33mdiff --git i/test/header_clean.bats w/test/header_clean.bats[m
[1;33mindex 4a3e7ee..a8385a5 100644[m
[1;33m--- i/test/header_clean.bats[m
[1;33m+++ w/test/header_clean.bats[m
[1;35m@@ -50,3 +50,8 @@[m [moutput=$( load_fixture "file-moves" | $diff_so_fancy )[m
assert_output --partial '@ setup-a-new-machine.sh:33 @'[m
assert_output --partial '@ setup-a-new-machine.sh:218 @'[m
}[m
[1;32m+[m
[1;32m+[m[1;32m@test "mnemonicprefix" {[m
[1;32m+[m [1;32moutput=$( load_fixture "mnemonicprefix" | $diff_so_fancy )[m
[1;32m+[m [1;32massert_output --partial '@ setup-a-new-machine.sh:33 @'[m
[1;32m+[m[1;32m}[m
[38;5;11mdiff --git c/hello.txt i/hello.txt[m
[38;5;11mdeleted file mode 100644[m
[38;5;11mindex 0c767bc..0000000[m
[38;5;11m--- c/hello.txt[m
[38;5;11m+++ /dev/null[m
[36m@@ -1 +0,0 @@[m
[38;5;10m-[m[38;5;10mHI THERE[m
================================================
FILE: test/fixtures/move_with_content_change.diff
================================================
diff --git a/a.txt b/b.txt
similarity index 84%
rename from a.txt
rename to b.txt
index 6674929..1c877ab 100644
--- a/a.txt
+++ b/b.txt
@@ -2,4 +2,4 @@ height: '10px',
width: '100%',
display: 'block',
position: 'absolute',
-bottom: '0',
+bottom: '0'
================================================
FILE: test/fixtures/noprefix.diff
================================================
[1;33mdiff --git setup-a-new-machine.sh setup-a-new-machine.sh[m
[1;33mindex 7b5996c..67eec2a 100755[m
[1;33m--- setup-a-new-machine.sh[m
[1;33m+++ setup-a-new-machine.sh[m
[1;35m@@ -30,6 +30,7 @@[m [mcp -R ~/.gnupg ~/migration/home[m
cp /Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist ~/migration # wifi[m
[m
cp ~/Library/Preferences/net.limechat.LimeChat.plist ~/migration[m
[1;32m+[m[1;32mcp ~/Library/Preferences/com.tinyspeck.slackmacgap.plist ~/migration[m
[m
cp -R ~/Library/Services ~/migration # automator stuff[m
[m
[1;35m@@ -215,5 +216,7 @@[m [msh .osx[m
# symlink it up![m
./symlink-setup.sh[m
[m
[1;32m+[m[1;32m# add manual symlink for .ssh/config and probably .config/fish[m
[1;32m+[m
###[m
##############################################################################################################[m
================================================
FILE: test/fixtures/recursive_default_as_mercurial.diff
================================================
diff -r -bu core/app.py language/app.py
--- core/app.py 2022-06-08 13:42:10.658920131 +0900
+++ language/app.py 2022-06-08 12:07:22.773069512 +0900
@@ -1,13 +1,13 @@
-from flask import Flask, abort
-from . import CORE_MODULES
+from flask import Flask
+from . import SECTION
app = Flask(__name__)
-@app.route('/<name>')
+@app.route(f'/{SECTION}/<name>')
def index(name):
- if name in CORE_MODULES:
- return "Welcome to the documentation for the core module"
- abort(404)
+ return f"Welcome to the documentation for {name}"
+if __name__ == "__main__":
+ app.run(host="0.0.0.0",port=3000)
diff -r -bu core/__init__.py language/__init__.py
--- core/__init__.py 2022-06-08 12:16:35.203331282 +0900
+++ language/__init__.py 2022-06-08 12:03:32.464264288 +0900
@@ -1 +1 @@
-CORE_MODULES=['base','utils','status']
+SECTION="language"
diff -r -bu core/README.md language/README.md
--- core/README.md 2022-06-08 13:52:56.962174912 +0900
+++ language/README.md 2022-06-08 13:52:39.498090643 +0900
@@ -1,4 +1,4 @@
-# Core
+# Language
## Installation
================================================
FILE: test/fixtures/recursive_longhand_as_mercurial.diff
================================================
diff --recursive -u core/app.py language/app.py
--- core/app.py 2022-06-08 13:42:10.658920131 +0900
+++ language/app.py 2022-06-08 12:07:22.773069512 +0900
@@ -1,13 +1,13 @@
-from flask import Flask, abort
-from . import CORE_MODULES
+from flask import Flask
+from . import SECTION
app = Flask(__name__)
-@app.route('/<name>')
+@app.route(f'/{SECTION}/<name>')
def index(name):
- if name in CORE_MODULES:
- return "Welcome to the documentation for the core module"
- abort(404)
+ return f"Welcome to the documentation for {name}"
+if __name__ == "__main__":
+ app.run(host="0.0.0.0",port=3000)
diff --recursive -u core/__init__.py language/__init__.py
--- core/__init__.py 2022-06-08 12:16:35.203331282 +0900
+++ language/__init__.py 2022-06-08 12:03:32.464264288 +0900
@@ -1 +1 @@
-CORE_MODULES=['base','utils','status']
+SECTION="language"
diff --recursive -u core/README.md language/README.md
--- core/README.md 2022-06-08 13:52:56.962174912 +0900
+++ language/README.md 2022-06-08 13:52:39.498090643 +0900
@@ -1,4 +1,4 @@
-# Core
+# Language
## Installation
================================================
FILE: test/fixtures/remove_empty_file.diff
================================================
commit 0148734e753690a9207ebcb8cd117f343e230dec
Author: Scott Baker <fake@domain.com>
Date: Wed Jul 19 08:12:45 2017 -0700
remvo
diff --git a/empty_file.txt b/empty_file.txt
deleted file mode 100644
index e69de29..0000000
================================================
FILE: test/fixtures/remove_slashn_eof.diff
================================================
diff --git a/test.txt b/test.txt
index 4cb29ea..54d55bf 100644
--- a/test.txt
+++ b/test.txt
@@ -1,3 +1,3 @@
one
two
-three
+three
\ No newline at end of file
================================================
FILE: test/fixtures/single-line-remove.diff
================================================
diff --git test/data/readywaitasset.js test/data/readywaitasset.js
deleted file mode 100644
index 2308965..0000000
--- test/data/readywaitasset.js
+++ /dev/null
@@ -1 +0,0 @@
-var delayedMessage = "It worked";
================================================
FILE: test/fixtures/truecolor.diff
================================================
[33mdiff --git package.json package.json[m
[33mindex 97965ab..f3ce90a 100644[m
[33m--- package.json[m
[33m+++ package.json[m
[1;35m@@ -13,8 +13,8 @@[m
"url": "git+https://github.com/so-fancy/diff-so-fancy.git"[m
},[m
"keywords": [[m
[1;38;2;220;50;47;48;2;0;43;54m- "git",[m
"diff",[m
[1;38;2;133;153;0;48;2;0;43;54m+[m[1;38;2;133;153;0;48;2;0;43;54m "git",[m
"fancy",[m
"good-lookin'",[m
"diff-highlight",[m
================================================
FILE: test/fixtures/unicode.diff
================================================
[1;33mdiff --git a/unicodes b/unicodes[m
[1;33mindex 223f57d..1c2066d 100644[m
[1;33m--- a/unicodes[m
[1;33m+++ b/unicodes[m
[1;35m@@ -1 +1 @@[m
[1;31m-aao[m
[1;32m+[m[1;32måäöç[m
================================================
FILE: test/fixtures/weird.diff
================================================
diff -r 62e478a3f1c8 -r 47aeb87ce9cd doc/manual.xml.head
--- a/doc/manual.xml.head
+++ b/doc/manual.xml.head
@@ -8352,7 +8352,7 @@
<row><entry>-a</entry><entry>attach a file to a message</entry></row>
<row><entry>-b</entry><entry>specify a blind carbon-copy (BCC) address</entry></row>
<row><entry>-c</entry><entry>specify a carbon-copy (Cc) address</entry></row>
-<row><entry>-d</entry><entry>log debugging output to ~/.muttdebug0 if mutt was complied with +DEBUG; it can range from 1-5 and affects verbosity (a value of 2 is recommended)</entry></row>
+<row><entry>-d</entry><entry>log debugging output to ~/.muttdebug0 if mutt was compiled with +DEBUG; it can range from 1-5 and affects verbosity (a value of 2 is recommended)</entry></row>
<row><entry>-D</entry><entry>print the value of all Mutt variables to stdout</entry></row>
<row><entry>-E</entry><entry>edit the draft (-H) or include (-i) file</entry></row>
<row><entry>-e</entry><entry>specify a config command to be run after initialization files are read</entry></row>
================================================
FILE: test/git-config.bats
================================================
#!/usr/bin/env bats
# Used by both `setup_file` and `setup`, which are special bats callbacks.
__load_imports__() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
load 'test_helper/util'
}
# ansi related values
escape=$'\e'
ansi_bold=1
ansi_dim=2
ansi_ul=4
ansi_reverse=7
# hash of colors
declare -Ag ansi_colors=(
[black]='0'
[red]='1'
[green]='2'
[yellow]='3'
[blue]='4'
[magenta]='5'
[cyan]='6'
[white]='7'
[default]='9'
)
# get a foreground or background color code
ansi_color() {
color=$1
incr=$2
base_code=$((30+$incr))
# handle bright prefix
if [[ "${1}" =~ (bright)(.*) ]]
then
color=${BASH_REMATCH[2]}
base_code=$((90+$incr))
fi
code=$(($base_code+${ansi_colors[$color]}))
echo "$code"
}
# get a foreground color code
fg_color() {
ansi_color $1 0
}
# get a background color code
bg_color() {
ansi_color $1 10
}
# get rgb color codes from hex
rgb_color() {
incr=$2
base_code=$((38+$incr))
[[ $1 =~ ^.(..)(..)(..)$ ]]
rgb1="${BASH_REMATCH[1]}"
rgb2="${BASH_REMATCH[2]}"
rgb3="${BASH_REMATCH[3]}"
echo "${base_code};2;$(( 16#${rgb1} ));$(( 16#${rgb2} ));$(( 16#${rgb3} ))"
}
# get a foreground color code
fg_rgb_color() {
rgb_color $1 0
}
# get a background color code
bg_rgb_color() {
rgb_color $1 10
}
# build config using passed in values
setup_dsf_git_config() {
GIT_CONFIG="$(dsf_test_git_config)" || return $?
cat > "${GIT_CONFIG}" <<EOF || return $?
[color "diff"]
$1
EOF
export GIT_CONFIG
export GIT_CONFIG_NOSYSTEM=1
}
setup_file() {
__load_imports__
# setup_default_dsf_git_config
}
setup() {
__load_imports__
}
teardown_file() {
teardown_default_dsf_git_config
}
# General description of how colors are applied
# meta = header
# frag = @ filenames
# func = function name after frag
# old = - lines
# new = + lines
@test "Test color attributes are removed" {
config="
meta = bold ul blink italic strike green no-reverse
frag = nobold nodim noul noblink noreverse noitalic nostrike blue white
func = dim reverse white purple
old = no-bold no-dim no-ul no-blink no-reverse no-italic no-strike red yellow
new = brightgreen
whitespace = red reverse
"
setup_dsf_git_config "$config"
output=$( load_fixture "leading-dashes" | $diff_so_fancy )
run printf "%s" "$output"
fg_green=$(fg_color green)
fg_blue=$(fg_color blue)
fg_red=$(fg_color red)
fg_brightgreen=$(fg_color brightgreen)
bg_white=$(bg_color white)
bg_yellow=$(bg_color yellow)
assert_line --index 0 --partial "${escape}[${ansi_bold};${ansi_ul};${fg_green}m"
assert_line --index 3 --partial "${escape}[${fg_blue};${bg_white}m@"
assert_line --index 6 --partial "${escape}[${fg_red};${bg_yellow}m--"
assert_line --index 7 --partial "${escape}[${fg_brightgreen}m--"
}
@test "Test normal and default git colors" {
config="
frag = normal default
old = default
"
setup_dsf_git_config "$config"
output=$( load_fixture "leading-dashes" | $diff_so_fancy )
run printf "%s" "$output"
fg_default=$(fg_color default)
bg_default=$(bg_color default)
assert_line --index 3 --partial "${escape}[${bg_default}m@"
assert_line --index 6 --partial "${escape}[${fg_default}m--"
}
# Special characters in git config must be quoted (word or all values)
@test "Test rgb git colors" {
rgb="#408050"
config="
frag = \"${rgb}\" default
old = normal \"${rgb}\"
"
setup_dsf_git_config "$config"
output=$( load_fixture "leading-dashes" | $diff_so_fancy )
run printf "%s" "$output"
fg_rgb=$(fg_rgb_color "${rgb}")
bg_default=$(bg_color default)
bg_rgb=$(bg_rgb_color "${rgb}")
assert_line --index 3 --partial "${escape}[${fg_rgb};${bg_default}m@"
assert_line --index 6 --partial "${escape}[${bg_rgb}m--"
}
================================================
FILE: test/git_ansi_color.pl
================================================
#!/usr/bin/env perl
use strict;
use warnings;
use v5.16;
###############################################################################
###############################################################################
compare_color("\e[1;91m" , git_ansi_color("brightred bold") , "brightred bold");
compare_color("\e[1;31m" , git_ansi_color("red bold") , "red bold");
compare_color("\e[1;32m" , git_ansi_color("green bold") , "green bold");
compare_color("\e[33m" , git_ansi_color("yellow") , "yellow");
compare_color("\e[1;7;32m" , git_ansi_color("green bold reverse"), "green bold reverse");
compare_color("\e[1;35m" , git_ansi_color("magenta bold") , "magenta bold");
compare_color("\e[1;38;5;146m" , git_ansi_color("146 bold") , "146 bold");
compare_color("\e[1;38;5;146;48;5;22m" , git_ansi_color("146 bold 22") , "146 bold 22");
compare_color("\e[1;34;40m" , git_ansi_color("blue black bold") , "blue black gold");
compare_color("\e[93m" , git_ansi_color("11") , "11");
compare_color("\e[7;31m" , git_ansi_color("red reverse") , "red reverse");
compare_color("\e[1;31;48;5;52m" , git_ansi_color("red bold 52") , "red bold 52");
compare_color("\e[92;48;5;20m" , git_ansi_color("10 20") , "10 20");
compare_color("\e[30;47m" , git_ansi_color("0 7") , "0 7");
compare_color("\e[94;105m" , git_ansi_color("12 13") , "12 13");
compare_color("\e[1;38;5;254;48;5;255m", git_ansi_color("254 bold 255") , "254 bold 255");
compare_color("\e[1;38;5;238;42m" , git_ansi_color("238 bold green") , "238 bold green");
compare_color("\e[1;38;5;238;42m" , git_ansi_color("238 green bold") , "238 green bold");
###############################################################################
###############################################################################
sub compare_color {
my ($one, $two, $desc) = @_;
if ($one ne $two) {
#k($one, $two);
#printf("No match for %-20s %s / %s\n", $desc, ansi_to_human($one), ansi_to_human($two));
printf("%-20s %sFAIL%s %s ne %s\n", $desc, color('red'), color(), ansi_to_human($one), ansi_to_human($two));
} else {
printf("%-20s %sOK%s\n", $desc, color('green'), color());
}
}
# Convert an ANSI sequence to something printable
sub ansi_to_human {
my $str = shift();
$str =~ s/\e\[/[/g;
$str =~ s/m\b/]/g;
return $str;
}
# Alternate (unused) method to parse git config colors
sub git_ansi_color2 {
my $str = shift();
my @parts = split(' ', $str);
if (!@parts) {
return '';
}
my %map = qw(
black 30 red 31 green 32 yellow 33 blue 34 magenta 35 cyan 36 white 37
bold 1 reverse 7
);
k(\@parts);
my @ret;
my $first = 1;
foreach my $item (@parts) {
my $val = $map{$item};
if (!$val) {
if (!$first && $item > 10) {
$val = $item + 10;
}
push(@ret, (38,5,$val));
} else {
# Background color
if (!$first && $val > 10) {
$val += 10;
$first = 0;
}
push(@ret, $val);
}
if ($val > 10) {
$first = 0;
}
}
my $ret = "\e[" . join(";", @ret) . "m";
print ansi_to_human($ret);
return $ret;
}
# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
sub git_ansi_color {
my $str = shift();
my @parts = split(' ', $str);
if (!@parts) {
return '';
}
my $colors = {
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
};
my @ansi_part = ();
if (grep { /bold/ } @parts) {
push(@ansi_part, "1");
}
if (grep { /reverse/ } @parts) {
push(@ansi_part, "7");
}
# Remove parts that aren't colors
@parts = grep { exists $colors->{$_} || is_numeric($_) } @parts;
my $fg = $parts[0] // "";
my $bg = $parts[1] // "";
#############################################
# It's an numeric value, so it's an 8 bit color
if (is_numeric($fg)) {
if ($fg < 8) {
push(@ansi_part, $fg + 30);
} elsif ($fg < 16) {
push(@ansi_part, $fg + 82);
} else {
push(@ansi_part, "38;5;$fg");
}
# It's a simple 16 color OG ansi
} elsif ($fg) {
my $bright = $fg =~ s/bright//;
my $color_num = $colors->{$fg} + 30;
if ($bright) { $color_num += 60; } # Set bold
push(@ansi_part, $color_num);
}
#############################################
# It's an numeric value, so it's an 8 bit color
if (is_numeric($bg)) {
if ($bg < 8) {
push(@ansi_part, $bg + 40);
} elsif ($bg < 16) {
push(@ansi_part, $bg + 92);
} else {
push(@ansi_part, "48;5;$bg");
}
# It's a simple 16 color OG ansi
} elsif ($bg) {
my $bright = $bg =~ s/bright//;
my $color_num = $colors->{$bg} + 40;
if ($bright) { $color_num += 60; } # Set bold
push(@ansi_part, $color_num);
}
#############################################
my $ansi_str = join(";", @ansi_part);
my $ret = "\e[" . $ansi_str . "m";
return $ret;
}
sub is_numeric {
my $s = shift();
if ($s =~ /^\d+$/) {
return 1;
}
return 0;
}
sub trim {
my $s = shift();
if (!defined($s) || length($s) == 0) { return ""; }
$s =~ s/^\s*//;
$s =~ s/\s*$//;
return $s;
}
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
my $str = shift();
# If we're NOT connected to a an interactive terminal don't do color
#if (-t STDOUT == 0) { return ''; }
# No string sent in, so we just reset
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
# Some predefined colors
my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
# Get foreground/background and any commands
my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
my ($bc) = $str =~ /on_(\d{1,3})$/g;
# Some predefined commands
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
my $cmd_num = $cmd_map{$cmd // 0};
my $ret = '';
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
return $ret;
}
# Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
if (eval { require Data::Dump::Color }) {
*k = sub { Data::Dump::Color::dd(@_) };
} else {
require Data::Dumper;
*k = sub { print Data::Dumper::Dumper(\@_) };
}
sub kd {
k(@_);
printf("Died at %2\$s line #%3\$s\n",caller());
exit(15);
}
}
# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4
================================================
FILE: test/test_helper/util.bash
================================================
diff_so_fancy="$BATS_TEST_DIRNAME/../diff-so-fancy"
load_fixture() {
local name="$1"
cat "$BATS_TEST_DIRNAME/fixtures/${name}.diff"
}
set_env() {
export LC_CTYPE="en_US.UTF-8"
}
dsf_test_git_config() {
printf '%s/gitconfig' "${BATS_TMPDIR}"
}
# applying colors so ANSI color values will match
# FIXME: not everyone will have these set, so we need to test for that.
setup_default_dsf_git_config() {
GIT_CONFIG="$(dsf_test_git_config)" || return $?
cat > "${GIT_CONFIG}" <<EOF || return $?
[color "diff"]
meta = 227
frag = magenta bold
commit = 227 bold
old = red bold
new = green bold
whitespace = red reverse
func = brightyellow
[color "diff-highlight"]
oldNormal = red bold
oldHighlight = red bold 52
newNormal = green bold
newHighlight = green bold 22
EOF
export GIT_CONFIG
export GIT_CONFIG_NOSYSTEM=1
}
teardown_default_dsf_git_config() {
local test_config
test_config="$(dsf_test_git_config)" || return $?
[ ! -f "${test_config}" ] || rm -f "${test_config}"
GIT_CONFIG=/dev/null
}
================================================
FILE: third_party/ansi-reveal/ansi-reveal.pl
================================================
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
my $raw = 0;
GetOptions(
'raw' => \$raw,
);
if ($raw) {
output_raw();
} else {
output_human();
}
###############################################
sub output_human {
my $reset = "\e[0m";
while (my $l = <>) {
$l =~ s/(\e\[.*?m)/dump_ansi($1)/eg;
## Basic reset
#$l =~ s/(\e\[0?m)/${reset}[RESET]/g;
## Bold text
#$l =~ s/(\e\[1m)/${reset}[BOLD]/g;
## Inverted text
#$l =~ s/(\e\[7m)/${reset}. "[INVRT]$1"/eg;
#$l =~ s/(\e\[7;(\d+)m)/${reset}. "[INVRT" . sprintf("%03d",($2-30)) . "]$1"/eg;
## Basic 16 color ANSI
##$l =~ s/(\e\[(3[0-7])m)/${reset}. "[BASIC" . sprintf("%03d",($2-30)) . "]$1"/eg;
#$l =~ s/(\e\[(3[0-7])m)/dump_ansi($1)/eg;
##$l =~ s/(\e\[1;(3[0-7])m)/${reset}. "[BRIGH" . sprintf("%03d",($2-30)) . "]$1"/eg;
#$l =~ s/(\e\[1;(3[0-7])m)/dump_ansi($1)/eg;
## 256 Color/Background
#$l =~ s/(\e\[38;0?5;(\d+)m)/${reset}. "[COLOR" . sprintf("%03d",$2) . "]$1"/eg;
#$l =~ s/(\e\[48;0?5;(\d+)m)/${reset}. "[BACKG" . sprintf("%03d",$2) . "]$1"/eg;
#$l =~ s/(\e\[1;(3[0-7]);48;5;(\d+)m)/${reset}. "[FGBG" . sprintf("%02d:%02d",($2-30),($3)) . "]$1"/eg;
#$l =~ s/(\e\[1;38;5;(.*?)m)/${reset} . dump_ansi($1) . "$1"/eg;
## 24bit Color/Background
#$l =~ s/(\e\[38;2;(\d+);(\d+);(\d+)m)/${reset} . sprintf("[RGB#%02X%02X%02X",$2,$3,$4) . "]$1"/eg;
#$l =~ s/(\e\[48;2;(\d+);(\d+);(\d+)m)/${reset} . sprintf("[RGBBG#%02X%02X%02X",$2,$3,$4) . "]$1"/eg;
print $l;
}
}
###############################################
sub output_raw {
while (<>) {
s/\e/\\e/g;
print;
}
}
sub dump_ansi {
my $str = shift();
if ($str !~ /^\e/) {
return "";
}
my $raw = $str;
my $human = $str =~ s/\e/ESC/rg;
# Remove the ANSI control chars, we just want the payload
$str =~ s/^\e\[//g;
$str =~ s/m$//g;
# Make the [HUMAN] text reset and white to make it easier to see
my $ret = "\e[0m";
$ret .= "\e[38;5;15m";
my @parts = split(";",$str);
#k(\@parts);
my @basic_mapping = qw(BLACK RED GREEN YELLW BLUE MAGNT CYAN WHITE);
if (!@parts) {
$ret .= "[RESET]";
}
for (my $count = 0; $count < @parts; $count++) {
my $p = $parts[$count];
#print "[$count = '$p']\n";
if ($p eq "1") {
$ret .= "[BOLD]";
} elsif ($p eq "0" || $p eq "") {
$ret .= "[RESET]";
} elsif ($p eq "7") {
$ret .= "[REVERSE]";
} elsif ($p eq "27") {
$ret .= "[NOTREV]";
} elsif ($p eq "38") {
my $next = $parts[$count + 1];
my $color = $parts[$count + 2];
$count += 2;
$ret .= sprintf("[COLOR%03d]",$color);
} elsif ($p eq "48") {
my $next = $parts[++$count];
my $color = $parts[++$count];
$count += 2;
$ret .= sprintf("[BACKG%03d]",$color);
} elsif ($p >= 30 and $p <= 37) {
my $color = $p - 30;
$color = $basic_mapping[$color];
$ret .= "[$color]";
#$ret .= sprintf("[BASIC%03d]",$color);
} else {
$ret .= "[UKN: $p]";
}
}
# Append the ANSI color string to end of the human readable one
$ret .= $raw;
return $ret;
}
BEGIN {
if (eval { require Data::Dump::Color }) {
*k = sub { Data::Dump::Color::dd($_[0]) };
} else {
require Data::Dumper;
*k = sub { print Data::Dumper::Dumper($_[0]) };
}
}
# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4
================================================
FILE: third_party/build_fatpack/build.pl
================================================
#!/usr/bin/env perl
###########################################################################
# This will build a stand-a-lone version of diff-so-fancy using Fatpack
#
# Scott Baker - 2017-06-28
#
# Usage: perl build.pl [--output /tmp/diff-so-fancy]
###########################################################################
use strict;
use warnings;
use File::Basename;
use Cwd qw(abs_path getcwd);
my $args = argv();
my $ok = has_fatpack();
if (!$ok) {
printf("%sError:%s App::FatPacker must be installed to build diff-so-fancy\n",color('red_bold'),color('reset'));
exit;
}
my $output_file = "/tmp/diff-so-fancy";
my $input_file = "diff-so-fancy";
# Allow overriding the output file
if ($args->{output}) {
$output_file = $args->{output};
}
my $dir = dirname($0);
my $root = abs_path("$dir/../../");
my $cwd = getcwd();
# Change to the root of the tree
chdir($root);
my $dsf_version = get_version($input_file);
my $cmd = "fatpack pack $input_file 2>/dev/null > $output_file";
my $exit = system($cmd);
$exit >>= 8;
rmdir("fatlib"); # fatpack leaves empty fatlib dirs so we remove them
my $size = -s $output_file;
my $good = color("82bold");
my $bad = color("160bold");
my $warn = color("226bold");
my $vers = color("230bold");
my $white = color("15bold");
my $reset = color();
if (!$exit) {
print "${good}Success:${reset} Wrote diff-so-fancy ${vers}v$dsf_version${reset} to $output_file ($size bytes)\n";
chmod 0755,$output_file; # Make the output executable
} else {
print "${bad}Error :${reset} Fatpack failed to build $output_file with exit code: ${warn}$exit${reset}\n";
print "${white}Command: $cmd$reset\n";
}
chdir($cwd);
#############################################################################
sub argv {
my $ret = {};
for (my $i = 0; $i < scalar(@ARGV); $i++) {
# If the item starts with "-" it's a key
if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_]\w*)/) && ($ARGV[$i] !~ /^-\w\w/)) {
# If the next item does not start with "--" it's the value for this item
if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) {
$ret->{$key} = $ARGV[$i + 1];
# Bareword like --verbose with no options
} else {
$ret->{$key}++;
}
}
}
# We're looking for a certain item
if ($_[0]) { return $ret->{$_[0]}; }
return $ret;
}
sub pfile {
my $file = shift();
if (!-r $file) { return ''; } # Make sure the file is readable
my $ret = '';
open(INPUT, "<", $file);
while (<INPUT>) {
$ret .= $_;
}
close INPUT;
if (wantarray) {
return split(/\n/,$ret);
} else {
return $ret;
}
}
sub get_version {
my $file = shift();
my @lines = pfile($file);
foreach my $l (@lines) {
if ($l =~ /\$VERSION\s+=\s+"(.+?)"/) {
my $ver = $1;
return $ver;
}
}
return undef;
}
sub has_fatpack {
my $out = `which fatpack 2>/dev/null`;
return trim($out);
}
sub trim {
my $s = shift();
if (length($s) == 0) { return ""; }
$s =~ s/^\s*|\s*$//g;
return $s;
}
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173'
sub color {
my $str = shift();
# If we're NOT connected to a an interactive terminal don't do color
if (-t STDOUT == 0) { return ''; }
# No string sent in, so we just reset
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
# Some predefined colors
my %color_map = qw(red 160 blue 21 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
$str =~ s/$_/$color_map{$_}/g for keys %color_map;
# Get foreground/background and any commands
my ($fc,$cmd) = $str =~ /^(\d+)?_?(\w+)?/g;
my ($bc) = $str =~ /on_?(\d+)$/g;
# Some predefined commands
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
my $cmd_num = $cmd_map{$cmd || 0};
my $ret = '';
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
return $ret;
}
# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4
================================================
FILE: third_party/cli_bench/cli_bench.pl
================================================
#!/usr/bin/env perl
use strict;
use warnings;
use v5.16;
###############################################################################
# Used to benchmark optimizations to CLI scripts
#
# Usage: cli_bench [--num 50] 'cat /tmp/diff.patch | diff-so-fancy'
###############################################################################
use Time::HiRes qw(time);
use Getopt::Long;
###############################################################################
###############################################################################
my $num = 50;
my $ignore = 1;
my $details = 1;
my $ok = GetOptions(
'num=i' => \$num,
'ignore=i' => \$ignore,
'details!' => \$details,
);
my $cmd = trim(join(" ", @ARGV));
if (!$cmd) {
die(usage());
}
$| = 0; # Disable output buffering
my @res;
my $out;
my $exit = 0;
for (my $i = 0; $i < ($num + $ignore); $i++) {
my $start = time();
$out = `$cmd`;
$exit = $? >> 8;
my $total = int((time() - $start) * 1000);
push(@res, $total);
print ".";
}
print "\n";
# Throw away the first X to give things time to warm up and be cached
@res = splice(@res, $ignore);
# Remove the top and bottom 10%
my $outlier = $num / 10;
@res = sort(@res);
@res = splice(@res, $outlier, $num - $outlier * 2);
my $avg = sprintf("%.1f", average(@res));
if ($details) {
show_details(@res);
print "\n";
}
print "Ran '$cmd' $num times with average completion time of $avg ms\n";
if ($exit != 0) {
print $out;
}
###############################################################################
###############################################################################
sub show_details {
my @res = @_;
my $x = {};
my $max = 0;
# Build a hash of all the times:count
foreach my $time (@res) {
$x->{$time}++;
if ($x->{$time} > $max) {
$max = $x->{$time};
}
}
my $target_width = 100; # How wide we want the bar + text
my $total = scalar(@res);
my $scale = ($target_width - 15) / $max;
print "\n";
# Print out a basic histogram of the times
foreach my $time (sort(keys %$x)) {
my $count = $x->{$time};
my $percent = sprintf("%0.1f", ($count / $total) * 100);
my $bar = "%" x ($count * $scale);
print "$time ms: $bar ($percent%)\n";
}
}
sub average {
my $ret = 0;
foreach (@_) {
$ret += $_;
}
my $count = scalar(@_);
$ret /= $count;
return $ret;
}
sub random_int {
my $ret = rand() * 90 + 10;
$ret = int($ret);
return $ret;
}
sub round {
my $num = shift();
#https://stackoverflow.com/questions/178539/how-do-you-round-a-floating-point-number-in-perl
#my $ret = int($num + $num/abs($num * 2 || 1));
my $ret;
if ($num < 0) {
$ret = int($num - 0.5);
} else {
$ret = int($num + 0.5);
}
return $ret;
}
sub trim {
my $s = shift();
if (!defined($s) || length($s) == 0) { return ""; }
$s =~ s/^\s*//;
$s =~ s/\s*$//;
return $s;
}
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
sub color {
my $str = shift();
# If we're NOT connected to a an interactive terminal don't do color
if (-t STDOUT == 0) { return ''; }
# No string sent in, so we just reset
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
# Some predefined colors
my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
# Get foreground/background and any commands
my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
my ($bc) = $str =~ /on_(\d{1,3})$/g;
# Some predefined commands
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
my $cmd_num = $cmd_map{$cmd // 0};
my $ret = '';
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
return $ret;
}
sub file_get_contents {
my $file = shift();
open (my $fh, "<", $file) or return undef;
my $ret;
while (<$fh>) { $ret .= $_; }
return $ret;
}
sub file_put_contents {
my ($file, $data) = @_;
open (my $fh, ">", $file) or return undef;
print $fh $data;
return length($data);
}
# Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper
# Creates methods k() and kd() to print, and print & die respectively
BEGIN {
if (eval { require Data::Dump::Color }) {
*k = sub { Data::Dump::Color::dd(@_) };
} else {
require Data::Dumper;
*k = sub { print Data::Dumper::Dumper(\@_) };
}
sub kd {
k(@_);
printf("Died at %2\$s line #%3\$s\n",caller());
exit(15);
}
}
sub usage {
return "Usage: $0 [--num 50] 'cat /tmp/simple.diff | diff-so-fancy'\n";
}
# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4
================================================
FILE: third_party/term-colors/term-colors.pl
================================================
#!/usr/bin/perl
use strict;
use warnings;
my $args = join(" ",@ARGV);
my ($perl) = $args =~ /--perl/;
my ($both) = $args =~ /--both/;
# If we want both, we set perl also
if ($both) {
$perl = 1;
}
# Term::ANSIColor didn't get 256 color constants until 4.0
if ($perl && has_term_ansicolor(4.0)) {
require Term::ANSIColor;
Term::ANSIColor->import(':constants','color','uncolor');
#print "TERM::ANSIColor constant names:\n";
term_ansicolor();
} else {
my $padding = 2;
my $section = 1;
my $grouping = 8;
for (my $i = 0; $i < 256; $i++) {
print set_bcolor($i); # Set the background color
if (needs_white($i)) {
print set_fcolor(15); # White
} else {
print set_fcolor(0); # Black
}
printf(" " x $padding . "%03d" . " " x $padding,$i);
print set_fcolor(); # Reset both colors
print " "; # Seperator
if ($i == 15 || $i == 231) {
print set_bcolor(); # Reset
print "\n\n";
$section = 0;
$grouping = 6;
$padding = 4;
} elsif ($section > 0 && ($section % $grouping == 0)) {
print set_bcolor(); # Reset
print "\n";
}
$section++;
}
}
END {
print set_fcolor(); # Reset the colors
print "\n";
}
#################################################################################
sub has_term_ansicolor {
my $version = shift();
eval {
# Check if we have Term::ANSIColor version 4.0
require Term::ANSIColor;
Term::ANSIColor->VERSION($version);
};
if ($@) {
return 0;
} else {
return 1;
}
}
sub set_fcolor {
my $c = shift();
my $ret = '';
if (!defined($c)) { $ret = "\e[0m"; } # Reset the color
else { $ret = "\e[38;5;${c}m"; }
return $ret;
}
sub set_bcolor {
my $c = shift();
my $ret = '';
if (!defined($c)) { $ret = "\e[0m"; } # Reset the color
else { $ret .= "\e[48;5;${c}m"; }
return $ret;
}
sub get_color_mapping {
my $map = {};
for (my $i = 0; $i < 256; $i++) {
my $str = "\e[38;5;${i}m";
my ($acc) = uncolor($str);
$map->{$acc} = int($i);
}
return $map;
}
sub term_ansicolor {
my @colors = get_color_names();
my $map = get_color_mapping();
my $absolute = 0;
my $group = 0;
my $grouping = 8;
print "Showing Term::ANSIColor constant names\n\n";
foreach my $name (@colors) {
my $bg = "on_$name";
my $map_num = int($map->{$name});
my $perl_name = sprintf("%6s",$name);
my $ansi_number = sprintf("#%03i",$map_num);
my $name_string = "";
if ($both) {
$name_string = "$perl_name / $ansi_number";
} else {
$name_string = "$perl_name";
}
if (needs_white($map_num)) {
print color($bg) . " " . color('bright_white') . $name_string . " ";
} else {
print color($bg) . " " . color("black") . $name_string . " ";
}
print color('reset') . " ";
$absolute++;
$group++;
if ($absolute == 16 || $absolute == 232) {
print "\n\n";
$group = 0;
$grouping = 6;
} elsif ($group % $grouping == 0) {
print "\n";
}
}
}
sub get_color_names {
my @colors = ();
my ($r,$g,$b) = 0;
for (my $i = 0; $i < 16; $i++) {
my $name = "ansi$i";
push(@colors,$name);
}
for ($r = 0; $r <= 5; $r++) {
for ($g = 0; $g <= 5; $g++) {
for ($b = 0; $b <= 5; $b++) {
my $name = "rgb$r$g$b";
push(@colors,$name);
}
}
}
for (my $i = 0; $i < 24; $i++) {
my $name = "grey$i";
push(@colors,$name);
}
return @colors;
}
sub needs_white {
# Sorta lame, but it's a hard coded list of which background colors need a white foreground
my @needs_white = qw(0 1 4 5 8 232 233 234 235 236 237 238 239 240 241 242 243 16 17 18
19 20 21 22 28 52 53 54 55 25 56 57 58 59 60 88 89 90 91 92 93 124 125 29 30 31 26
27 61 62 64 160 196 161 126 63 94 95 100 101 127 128 129 12 130 131 23 24);
my $num = shift();
my $ret = in_array($num, @needs_white);
return $ret;
}
sub in_array {
my ($needle, @haystack) = @_;
foreach my $l (@haystack) {
if ($l == $needle) { return 1; }
}
return 0;
}
================================================
FILE: update-deps.sh
================================================
#!/bin/bash
# initialize the bats components
git submodule sync && git submodule update --init
gitextract_6y16t5y0/ ├── .circleci/ │ └── config.yml ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── awesomebot.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── diff-so-fancy ├── diff-so-fancy.plugin.zsh ├── docs/ │ └── diff-so-fancy.1 ├── hacking-and-testing.md ├── history.md ├── lib/ │ └── DiffHighlight.pm ├── package.json ├── pro-tips.md ├── report-bug.sh ├── reporting-bugs.md ├── test/ │ ├── bugs.bats │ ├── diff-so-fancy.bats │ ├── fixtures/ │ │ ├── add_empty_file.diff │ │ ├── add_remove_empty_lines.diff │ │ ├── ansi_reset_no_number.diff │ │ ├── binary-modified.diff │ │ ├── chromium-modaltoelement.diff │ │ ├── complex-hunks.diff │ │ ├── diff_recursive.diff │ │ ├── dotfiles.diff │ │ ├── file-moves.diff │ │ ├── file-perms.diff │ │ ├── file-rename.diff │ │ ├── file_copy.diff │ │ ├── file_with_space.diff │ │ ├── first-three-line.diff │ │ ├── hg.diff │ │ ├── hunk_no_comma.diff │ │ ├── latin1.diff │ │ ├── leading-dashes.diff │ │ ├── ls-function.diff │ │ ├── mnemonicprefix.diff │ │ ├── move_with_content_change.diff │ │ ├── noprefix.diff │ │ ├── recursive_default_as_mercurial.diff │ │ ├── recursive_longhand_as_mercurial.diff │ │ ├── remove_empty_file.diff │ │ ├── remove_slashn_eof.diff │ │ ├── single-line-remove.diff │ │ ├── truecolor.diff │ │ ├── unicode.diff │ │ └── weird.diff │ ├── git-config.bats │ ├── git_ansi_color.pl │ └── test_helper/ │ └── util.bash ├── third_party/ │ ├── ansi-reveal/ │ │ └── ansi-reveal.pl │ ├── build_fatpack/ │ │ └── build.pl │ ├── cli_bench/ │ │ └── cli_bench.pl │ └── term-colors/ │ └── term-colors.pl └── update-deps.sh
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (173K chars).
[
{
"path": ".circleci/config.yml",
"chars": 424,
"preview": "version: 2\njobs:\n build:\n machine: true\n steps:\n - checkout\n - run:\n name:\n command: "
},
{
"path": ".github/dependabot.yml",
"chars": 179,
"preview": "---\n# Use `allow` to specify which dependencies to maintain\n\nversion: 2\nupdates:\n - package-ecosystem: \"github-actions\""
},
{
"path": ".github/workflows/awesomebot.yml",
"chars": 1326,
"preview": "---\nname: Check links in README.md\n\non:\n # Run daily\n schedule: [{cron: \"0 1 * * *\"}]\n push:\n branches: [\"*\"]\n pu"
},
{
"path": ".github/workflows/publish.yml",
"chars": 952,
"preview": "name: Publish to npm\n\non:\n push:\n tags:\n - 'v*'\n\njobs:\n publish:\n runs-on: ubuntu-latest\n permissions:\n "
},
{
"path": ".gitignore",
"chars": 7,
"preview": "/dist/\n"
},
{
"path": ".gitmodules",
"chars": 359,
"preview": "[submodule \"test/bats\"]\n\tpath = test/bats\n\turl = https://github.com/bats-core/bats-core.git\n[submodule \"test/test_helper"
},
{
"path": ".npmignore",
"chars": 7,
"preview": "/test/\n"
},
{
"path": ".travis.yml",
"chars": 1031,
"preview": "os: linux\nlanguage: perl\nperl:\n - blead\n - dev\n - 5.30\n - 5.28\n - 5.26\n - 5.24\n - 5.22\n - 5.20\n - 5.18\n - 5.14"
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 So Fancy team\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.md",
"chars": 4121,
"preview": "# 🎩 diff-so-fancy [](https://circlec"
},
{
"path": "appveyor.yml",
"chars": 178,
"preview": "build: false\n\nbefore_test:\n - git submodule sync\n - git submodule update --init\n\ntest_script:\n - C:\\cygwin\\bin\\bash -"
},
{
"path": "diff-so-fancy",
"chars": 38394,
"preview": "#!/usr/bin/env perl\n\nmy $VERSION = \"1.4.7\";\n\n###########################################################################"
},
{
"path": "diff-so-fancy.plugin.zsh",
"chars": 394,
"preview": "# Add our plugin diretory to user's path\n#\n# See following web page for explanation of the line \"ZERO=...\":\n# https://zd"
},
{
"path": "docs/diff-so-fancy.1",
"chars": 4045,
"preview": ".\\\" -*- mode: troff; coding: utf-8 -*-\n.\\\" Automatically generated by Pod::Man 5.0102 (Pod::Simple 3.45)\n.\\\"\n.\\\" Standar"
},
{
"path": "hacking-and-testing.md",
"chars": 1627,
"preview": "### Hacking\n\n```sh\n# fork and clone the diff-so-fancy repo.\ngit clone https://github.com/so-fancy/diff-so-fancy/ && cd d"
},
{
"path": "history.md",
"chars": 610,
"preview": "## History\n\n`diff-so-fancy` started as [a commit in paulirish's dotfiles](https://github.com/paulirish/dotfiles/commit/6"
},
{
"path": "lib/DiffHighlight.pm",
"chars": 6785,
"preview": "package DiffHighlight;\n\nuse 5.008;\nuse warnings FATAL => 'all';\nuse strict;\n\n# Use the correct value for both UNIX and W"
},
{
"path": "package.json",
"chars": 966,
"preview": "{\n \"name\": \"diff-so-fancy\",\n \"version\": \"1.4.6\",\n \"description\": \"Good-lookin' diffs with diff-highlight and more\",\n "
},
{
"path": "pro-tips.md",
"chars": 2603,
"preview": "# Pro-tips\n\n## One-off fanciness or a specific diff-so-fancy alias\n\nYou can do also do a one-off:\n\n```shell\ngit diff --c"
},
{
"path": "report-bug.sh",
"chars": 1297,
"preview": "#!/bin/bash\n\nclipboard() {\n\tlocal copy_cmd\n\n\tif [ -n \"$PBCOPY_SERVER\" ]; then\n\t\tlocal body=\"\" # buffer\n\t\tbody=$(cat)\n\t\t#"
},
{
"path": "reporting-bugs.md",
"chars": 261,
"preview": "### Reporting bugs\n\nIf you find a bug using the following command\n\n```sh\ngit diff HEAD..HEAD^\n```\n\nYou can use [report-b"
},
{
"path": "test/bugs.bats",
"chars": 3212,
"preview": "#!/usr/bin/env bats\n\n# Used by both `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() {\n\tlo"
},
{
"path": "test/diff-so-fancy.bats",
"chars": 7873,
"preview": "#!/usr/bin/env bats\n\n# Helper invoked by `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() "
},
{
"path": "test/fixtures/add_empty_file.diff",
"chars": 235,
"preview": "commit ef0e63dbd7e8df6cde4ee5599ad65db7820888ef\nAuthor: Scott Baker <fake@domain.com>\nDate: Wed Jul 19 08:00:11 2017 -"
},
{
"path": "test/fixtures/add_remove_empty_lines.diff",
"chars": 128,
"preview": "--- one.txt\t2020-04-23 10:15:29.193452325 -0700\n+++ two.txt\t2020-04-23 10:15:37.634463619 -0700\n@@ -1,5 +1,5 @@\n 1\n+\n 2\n"
},
{
"path": "test/fixtures/ansi_reset_no_number.diff",
"chars": 834,
"preview": "\u001b[93mdiff --git a/history.md b/history.md\u001b[m\n\u001b[93mindex f6776e0..a6b4546 100644\u001b[m\n\u001b[93m--- a/history.md\u001b[m\n\u001b[93m+++ b/h"
},
{
"path": "test/fixtures/binary-modified.diff",
"chars": 935,
"preview": "\u001b[1;33mdiff --git a/cancel.png b/cancel.png\u001b[m\n\u001b[1;33mindex 667e10c..06fc49c 100644\u001b[m\nBinary files a/cancel.png and b/c"
},
{
"path": "test/fixtures/chromium-modaltoelement.diff",
"chars": 2109,
"preview": "\u001b[1;33mdiff --git a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js b/third_party/WebKit/Source/devtools/front"
},
{
"path": "test/fixtures/complex-hunks.diff",
"chars": 1326,
"preview": "\u001b[1;32mcommit 74804e377d4a54d1173d4393827d4e4b27e4d5d0\u001b[m\n\u001b[1;33mdiff --cc libs/header_clean/header_clean.pl\u001b[m\n\u001b[1;33mi"
},
{
"path": "test/fixtures/diff_recursive.diff",
"chars": 452,
"preview": "Only in /var/tmp/b: donk\ndiff --recursive -u /var/tmp/a/foo/bar /var/tmp/b/foo/bar\n--- /var/tmp/a/foo/bar 2026-03-13 21"
},
{
"path": "test/fixtures/dotfiles.diff",
"chars": 18612,
"preview": "\u001b[1;33mdiff --git a/.aliases b/.aliases\u001b[m\n\u001b[1;33mindex 30182ef..be9fb1d 100644\u001b[m\n\u001b[1;33m--- a/.aliases\u001b[m\n\u001b[1;33m+++ b"
},
{
"path": "test/fixtures/file-moves.diff",
"chars": 4081,
"preview": "\u001b[33mcommit a12f64cfa2388b1d07659149db3a77314c9836c8\u001b[m\nAuthor: Scott Baker <bakers@canbytel.com>\nDate: Thu Feb 25 08:"
},
{
"path": "test/fixtures/file-perms.diff",
"chars": 3053,
"preview": "\u001b[38;5;227mdiff --git a/circle.yml b/circle.yml\u001b[m\n\u001b[38;5;227mold mode 100644\u001b[m\n\u001b[38;5;227mnew mode 100755\u001b[m\n\u001b[38;5;22"
},
{
"path": "test/fixtures/file-rename.diff",
"chars": 460,
"preview": "diff --git a/Changes.new b/bin/Changes.new\nsimilarity index 100%\nrename from Changes.new\nrename to bin/Changes.new\ndiff "
},
{
"path": "test/fixtures/file_copy.diff",
"chars": 101,
"preview": "diff --git a/first_file b/copied_file\nsimilarity index 100%\ncopy from first_file\ncopy to copied_file\n"
},
{
"path": "test/fixtures/file_with_space.diff",
"chars": 67,
"preview": "diff --git a/a b b/a b\nnew file mode 100644\nindex 0000000..e69de29\n"
},
{
"path": "test/fixtures/first-three-line.diff",
"chars": 453,
"preview": "commit fbf440dd9c32a60f9bc97693a6bd7b5ca87ec9fc\nAuthor: Steve Mao <maochenyan@gmail.com>\nDate: Wed Mar 9 19:21:27 2016"
},
{
"path": "test/fixtures/hg.diff",
"chars": 442,
"preview": "diff -r 82e55d328c8c hello.c\n--- a/hello.c\tFri Aug 26 01:21:28 2005 -0700\n+++ b/hello.c\tFri Dec 29 14:37:26 2017 -0800\n@"
},
{
"path": "test/fixtures/hunk_no_comma.diff",
"chars": 104,
"preview": "diff --git i/file w/file\nindex 90be1f3..294186e 100644\n--- i/file\n+++ w/file\n@@ -1 +1 @@\n-before\n+after\n"
},
{
"path": "test/fixtures/latin1.diff",
"chars": 252,
"preview": "\u001b[38;5;227mdiff --git a/test.txt b/test.txt\u001b[m\n\u001b[38;5;227mindex 5836369..51ccf71 100644\u001b[m\n\u001b[38;5;227m--- a/test.txt\u001b[m\n"
},
{
"path": "test/fixtures/leading-dashes.diff",
"chars": 499,
"preview": "diff --git i/lib/awful/widget/keyboardlayout.lua w/lib/awful/widget/keyboardlayout.lua\nindex f9142b8..b6e8ce0 100644\n---"
},
{
"path": "test/fixtures/ls-function.diff",
"chars": 960,
"preview": "\u001b[1;33mdiff --git a/fish/functions/ls.fish b/fish/functions/ls.fish\u001b[m\n\u001b[1;33mindex 33c3d8b..fd54db2 100644\u001b[m\n\u001b[1;33m--"
},
{
"path": "test/fixtures/mnemonicprefix.diff",
"chars": 3057,
"preview": "\u001b[1;33mdiff --git i/diff-so-fancy w/diff-so-fancy\u001b[m\n\u001b[1;33mindex 2323d9b..a280985 100755\u001b[m\n\u001b[1;33m--- i/diff-so-fancy\u001b"
},
{
"path": "test/fixtures/move_with_content_change.diff",
"chars": 253,
"preview": "diff --git a/a.txt b/b.txt\nsimilarity index 84%\nrename from a.txt\nrename to b.txt\nindex 6674929..1c877ab 100644\n--- a/a."
},
{
"path": "test/fixtures/noprefix.diff",
"chars": 896,
"preview": "\u001b[1;33mdiff --git setup-a-new-machine.sh setup-a-new-machine.sh\u001b[m\n\u001b[1;33mindex 7b5996c..67eec2a 100755\u001b[m\n\u001b[1;33m--- se"
},
{
"path": "test/fixtures/recursive_default_as_mercurial.diff",
"chars": 1074,
"preview": "diff -r -bu core/app.py language/app.py\n--- core/app.py\t2022-06-08 13:42:10.658920131 +0900\n+++ language/app.py\t2022-06-"
},
{
"path": "test/fixtures/recursive_longhand_as_mercurial.diff",
"chars": 1098,
"preview": "diff --recursive -u core/app.py language/app.py\n--- core/app.py\t2022-06-08 13:42:10.658920131 +0900\n+++ language/app.py\t"
},
{
"path": "test/fixtures/remove_empty_file.diff",
"chars": 230,
"preview": "commit 0148734e753690a9207ebcb8cd117f343e230dec\nAuthor: Scott Baker <fake@domain.com>\nDate: Wed Jul 19 08:12:45 2017 -"
},
{
"path": "test/fixtures/remove_slashn_eof.diff",
"chars": 161,
"preview": "diff --git a/test.txt b/test.txt\nindex 4cb29ea..54d55bf 100644\n--- a/test.txt\n+++ b/test.txt\n@@ -1,3 +1,3 @@\n one\n two\n-"
},
{
"path": "test/fixtures/single-line-remove.diff",
"chars": 210,
"preview": "diff --git test/data/readywaitasset.js test/data/readywaitasset.js\ndeleted file mode 100644\nindex 2308965..0000000\n--- t"
},
{
"path": "test/fixtures/truecolor.diff",
"chars": 469,
"preview": "\u001b[33mdiff --git package.json package.json\u001b[m\n\u001b[33mindex 97965ab..f3ce90a 100644\u001b[m\n\u001b[33m--- package.json\u001b[m\n\u001b[33m+++ pac"
},
{
"path": "test/fixtures/unicode.diff",
"chars": 203,
"preview": "\u001b[1;33mdiff --git a/unicodes b/unicodes\u001b[m\r\n\u001b[1;33mindex 223f57d..1c2066d 100644\u001b[m\r\n\u001b[1;33m--- a/unicodes\u001b[m\r\n\u001b[1;33m++"
},
{
"path": "test/fixtures/weird.diff",
"chars": 1039,
"preview": "diff -r 62e478a3f1c8 -r 47aeb87ce9cd doc/manual.xml.head\n--- a/doc/manual.xml.head\n+++ b/doc/manual.xml.head\n@@ -8352,7 "
},
{
"path": "test/git-config.bats",
"chars": 3888,
"preview": "#!/usr/bin/env bats\n\n# Used by both `setup_file` and `setup`, which are special bats callbacks.\n__load_imports__() {\n\tlo"
},
{
"path": "test/git_ansi_color.pl",
"chars": 6748,
"preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse v5.16;\n\n#############################################################"
},
{
"path": "test/test_helper/util.bash",
"chars": 1028,
"preview": "\ndiff_so_fancy=\"$BATS_TEST_DIRNAME/../diff-so-fancy\"\n\nload_fixture() {\n local name=\"$1\"\n cat \"$BATS_TEST_DIRNAME/fixtu"
},
{
"path": "third_party/ansi-reveal/ansi-reveal.pl",
"chars": 3255,
"preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse Getopt::Long;\n\nmy $raw = 0;\nGetOptions(\n\t'raw' => \\$raw,\n);\n\nif ($raw"
},
{
"path": "third_party/build_fatpack/build.pl",
"chars": 3927,
"preview": "#!/usr/bin/env perl\n\n###########################################################################\n# This will build a sta"
},
{
"path": "third_party/cli_bench/cli_bench.pl",
"chars": 4683,
"preview": "#!/usr/bin/env perl\n\nuse strict;\nuse warnings;\nuse v5.16;\n\n#############################################################"
},
{
"path": "third_party/term-colors/term-colors.pl",
"chars": 3901,
"preview": "#!/usr/bin/perl\n\nuse strict;\nuse warnings;\n\nmy $args = join(\" \",@ARGV);\nmy ($perl) = $args =~ /--perl/;\nmy ($both) = $"
},
{
"path": "update-deps.sh",
"chars": 96,
"preview": "#!/bin/bash\n\n# initialize the bats components\ngit submodule sync && git submodule update --init\n"
}
]
About this extraction
This page contains the full source code of the so-fancy/diff-so-fancy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (145.6 KB), approximately 54.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.