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 [![Circle CI build](https://circleci.com/gh/so-fancy/diff-so-fancy.svg?style=shield)](https://circleci.com/gh/so-fancy/diff-so-fancy) [![AppVeyor build](https://ci.appveyor.com/api/projects/status/github/so-fancy/diff-so-fancy?branch=master&svg=true)](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` ![diff-highlight vs diff-so-fancy](diff-so-fancy.png) ## 📦 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 = ) { # 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 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 -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 Colorize the first block of an empty line. (Default: true) git config --bool --global diff-so-fancy.markEmptyLines false B Simplify Git header chunks to a human readable format. (Default: true) git config --bool --global diff-so-fancy.changeHunkIndicators false B Should the + or - symbols at line-start be removed. (Default: true) git config --bool --global diff-so-fancy.stripLeadingSymbols false B 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 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 () { 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 "@ 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 " -" refute_output --partial " +" } @test "empty lines added/removed are marked" { run printf "%s" "$output" assert_line --index 7 --partial " " assert_line --index 24 --partial " " #assert_output --partial " " #assert_output --partial " " } @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 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 ================================================ diff --git a/history.md b/history.md index f6776e0..a6b4546 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,3 @@ -## History +## Historyz  `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: test/fixtures/binary-modified.diff ================================================ diff --git a/cancel.png b/cancel.png index 667e10c..06fc49c 100644 Binary files a/cancel.png and b/cancel.png differ diff --git a/diff-so-fancy b/diff-so-fancy index 0f2911c..5811717 100755 --- a/diff-so-fancy +++ b/diff-so-fancy @@ -32,13 +32,6 @@ color_code_regex="(${CSI}([0-9]{1,3}(;[0-9]{1,3}){0,3})[m|K])?"  git_index_line_pattern="^($color_code_regex)index .*"  -format_diff_header () { - # simplify the unified patch diff header - $SED -E "/$git_index_line_pattern/{N;s/$git_index_line_pattern\n//;}" \ - | $SED -E "s/^($color_code_regex)\-\-\-(.*)$/\1$(print_horizontal_rule)\\${NL}\1---\5/g" \ - | $SED -E "s/^($color_code_regex)\+\+\+(.*)$/\1+++\5\\${NL}\1$(print_horizontal_rule)/g" -} - print_horizontal_rule () { let width="$(tput cols)"  ================================================ FILE: test/fixtures/chromium-modaltoelement.diff ================================================ diff --git a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js index 4f9adf8..8c13743 100644 --- a/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js +++ b/third_party/WebKit/Source/devtools/front_end/ui/Dialog.js @@ -32,7 +32,7 @@ * @constructor * @extends {WebInspector.Widget} */ -WebInspector.Dialog = function() +WebInspector.Dialog = function(isModalToElement) { WebInspector.Widget.call(this, true); this.markAsRoot(); @@ -45,6 +45,9 @@ WebInspector.Dialog = function()  this._wrapsContent = false; this._dimmed = false; + this._isModalToElement = isModalToElement; + + this._glassPane = new WebInspector.GlassPane(relativeToElement, isModalToElement); /** @type {!Map} */ this._tabIndexMap = new Map(); } @@ -62,16 +65,16 @@ WebInspector.Dialog.prototype = { /** * @override */ - show: function() + show: function(isModalToElement) { if (WebInspector.Dialog._instance) WebInspector.Dialog._instance.detach(); WebInspector.Dialog._instance = this;  - var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument); + var document = /** @type {!Document} */ (WebInspector.Dialog._modalHostView.element.ownerDocument, isModalToElement); this._disableTabIndexOnElements(document);  - this._glassPane = new WebInspector.GlassPane(document, this._dimmed); + this._glassPane = new WebInspector.GlassPane(document, isModalToElement); this._glassPane.element.addEventListener("click", this._onGlassPaneClick.bind(this), false); WebInspector.GlassPane.DefaultFocusedViewStack.push(this);  ================================================ FILE: test/fixtures/complex-hunks.diff ================================================ commit 74804e377d4a54d1173d4393827d4e4b27e4d5d0 diff --cc libs/header_clean/header_clean.pl index e8bcd92,5970580..ae279d0 --- a/libs/header_clean/header_clean.pl +++ b/libs/header_clean/header_clean.pl @@@ -105,13 -104,21 +104,23 @@@ for (my $i = 0; $i <= $#input; $i++)  } }  + # 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); + } +  sub strip_empty_first_line {  - my $foo = shift(); # Array passed in by reference  + my $array = shift(); # Array passed in by reference  # If the first line is just whitespace remove it  - if (defined($foo->[0]) && $foo->[0] =~ /^\s*$/) {  - shift($foo);  + if (defined($array->[0]) && $array->[0] =~ /^\s*$/) {  + shift(@$array); # Throw away the first line }  +  + return 1; } ================================================ 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 ================================================ diff --git a/.aliases b/.aliases index 30182ef..be9fb1d 100644 --- a/.aliases +++ b/.aliases @@ -18,6 +18,7 @@ alias brwe=brew #typos  alias hosts='sudo $EDITOR /etc/hosts' # yes I occasionally 127.0.0.1 twitter.com ;)  +alias ag='ag -W 200 -f'  ### # time to upgrade `ls` @@ -51,7 +52,7 @@ alias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup  # Networking. IP address, dig, DNS alias ip="dig +short myip.opendns.com @resolver1.opendns.com" -alias dig="dig +nocmd any +multiline +noall +answer" +# alias dig="dig +nocmd any +multiline +noall +answer"  # Recursively delete `.DS_Store` files alias cleanup_dsstore="find . -name '*.DS_Store' -type f -ls -delete" @@ -68,7 +69,13 @@ alias ungz="gunzip -k" alias fs="stat -f \"%z bytes\""  # Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage -alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage" +alias emptytrash=" \  + sudo rm -rfv /Volumes/*/.Trashes; \ + rm -rfv ~/.Trash/*; \ + sudo rm -v /private/var/vm/sleepimage; \ + rm -rv \"/Users/paulirish/Library/Application Support/stremio/Cache\"; \ + rm -rv \"/Users/paulirish/Library/Application Support/stremio/stremio-cache\" \ +"  # Update installed Ruby gems, Homebrew, npm, and their installed packages alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor" diff --git a/.bash_profile b/.bash_profile index 8f17751..def367d 100644 --- a/.bash_profile +++ b/.bash_profile @@ -116,3 +116,9 @@ shopt -s cdspell;    + +# The next line updates PATH for the Google Cloud SDK. +source '/Users/paulirish/google-cloud-sdk/path.bash.inc' + +# The next line enables shell command completion for gcloud. +source '/Users/paulirish/google-cloud-sdk/completion.bash.inc' diff --git a/.bash_prompt b/.bash_prompt index 852d69f..8d3e3d0 100644 --- a/.bash_prompt +++ b/.bash_prompt @@ -4,6 +4,9 @@ default_username='paulirish'   +eval "$(thefuck --alias)" + + if [[ -n "$ZSH_VERSION" ]]; then # quit now if in zsh return 1 2> /dev/null || exit 1; fi; diff --git a/.bashrc b/.bashrc index 877b68f..18461ac 100644 --- a/.bashrc +++ b/.bashrc @@ -1,2 +1,4 @@ [ -n "$PS1" ] && source ~/.bash_profile  + +# [ -f ~/.fzf.bash ] && source ~/.fzf.bash diff --git a/.functions b/.functions index 8292d2e..a548d45 100644 --- a/.functions +++ b/.functions @@ -28,6 +28,19 @@ cdf() { # short for cdfinder }   + +# git commit browser. needs fzf +log() { + git log --graph --color=always \ + --format="%C(auto)%h%d %s %C(black)%C(bold)%cr" "$@" | + fzf --ansi --no-sort --reverse --tiebreak=index --toggle-sort=\` \ + --bind "ctrl-m:execute: + echo '{}' | grep -o '[a-f0-9]\{7\}' | head -1 | + xargs -I % sh -c 'git show --color=always % | less -R'" +} + + + # Start an HTTP server from a directory, optionally specifying the port function server() { local port="${1:-8000}" @@ -145,17 +158,14 @@ webmify(){ ffmpeg -i $1 -vcodec libvpx -acodec libvorbis -isync -copyts -aq 80 -threads 3 -qmax 30 -y $2 $1.webm }  +# direct it all to /dev/null +function nullify() { + "$@" >/dev/null 2>&1 +} +  # visual studio code. a la `subl` -code () { - if [[ $# = 0 ]] - then - open -a "Visual Studio Code" - else - [[ $1 = /* ]] && F="$1" || F="$PWD/${1#./}" - open -a "Visual Studio Code" --args "$F" - fi -} +function code () { VSCODE_CWD="$PWD" open -n -b "com.microsoft.VSCode" --args $*; }  # `shellswitch [bash |zsh]` # Must be in /etc/shells diff --git a/.gitconfig b/.gitconfig index d2be05f..d32f98c 100644 --- a/.gitconfig +++ b/.gitconfig @@ -7,11 +7,13 @@ df = diff --color --color-words --abbrev lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -- co = checkout + cherry = cherry-pick # does this conflict with the real `git cherry`?  # Show the diff between the latest commit and the current state d = !"git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat"  - reup = rebase-update + # chromium/depot_tools convenience. manual + reup = rebase-update --no_fetch --keep-going    @@ -23,7 +25,7 @@ excludesfile = ~/.gitignore attributesfile = ~/.gitattributes # insanely beautiful diffs - pager = diff-highlight | diff-so-fancy | less -r + pager = diff-so-fancy | less --tabs=1,5 -R [color "branch"] current = yellow reverse local = yellow @@ -40,7 +42,8 @@ changed = green untracked = cyan [merge] - tool = opendiff + #tool = opendiff + tool = kdiff3   [color "diff-highlight"] @@ -86,3 +89,5 @@ required = true [init] templatedir = ~/.git_template +[http] + cookiefile = /Users/paulirish/.gitcookies diff --git a/.zshrc b/.zshrc index 410a2ca..dbad355 100644 --- a/.zshrc +++ b/.zshrc @@ -113,3 +113,5 @@ source ~/.bash_profile   export PATH="$PATH:$HOME/.rvm/bin" # Add RVM to PATH for scripting + +[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh diff --git a/README.md b/README.md index 496bbbb..171eaea 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ Lastly, I use `open .` to open Finder from this path. (That's just available nor ## overview of files  #### Automatic config -* `.sift.conf` - sift (faster than grep, ack, ag) * `.vimrc`, `.vim` - vim config, obv. * `.inputrc` - behavior of the actual prompt line  diff --git a/bin/diff-highlight b/bin/diff-highlight deleted file mode 120000 index 7c5c827..0000000 --- a/bin/diff-highlight +++ /dev/null @@ -1 +0,0 @@ -/Users/paulirish/.homebrew/share/git-core/contrib/diff-highlight/diff-highlight \ No newline at end of file diff --git a/bin/diff-so-fancy b/bin/diff-so-fancy deleted file mode 100755 index 5b004a2..0000000 --- a/bin/diff-so-fancy +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -############### -# diff-so-fancy builds on the good-lookin' output of diff-highlight to upgrade your diffs' appearances -# * Output will not be in standard patch format, but will be readable -# * No pesky `+` or `-` at line-stars, making for easier copy-paste. -# -# Screenshot: https://github.com/paulirish/dotfiles/commit/6743b907ff58#commitcomment-13349456 -# -# -# Usage -# -# git diff | diff-highlight | diff-so-fancy -# -# Add to .gitconfig so all `git diff` uses it. -# git config --global core.pager "diff-highlight | diff-so-fancy | less -r" -# -# -# Requirements / Install -# -# * GNU sed. On Mac, install it with Homebrew: -# brew install gnu-sed --default-names # You'll have to change below to `gsed` otherwise -# * diff-highlight. It's shipped with Git, but probably not in your $PATH -# ln -sf "$(brew --prefix)/share/git-core/contrib/diff-highlight/diff-highlight" ~/bin/diff-highlight -# * Add some coloring to your .gitconfig: -# 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" -# -############### - -# TODO: -# Put on NPM. - - -[ $# -ge 1 -a -f "$1" ] && input="$1" || input="-" - -color_code_regex=$'(\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)[m|K])?' -reset_color="\x1B\[m" -dim_magenta="\x1B\[38;05;146m" - -format_diff_header () { - # simplify the unified patch diff header - sed -E "s/^($color_code_regex)diff --git .*$//g" | \ - sed -E "s/^($color_code_regex)index .*$/\ -\1$(print_horizontal_rule)/g" | \ - sed -E "s/^($color_code_regex)\+\+\+(.*)$/\1\+\+\+\5\\ -\1$(print_horizontal_rule)/g" -} - -colorize_context_line () { - # extra color for @@ context line - sed -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g" -} - -strip_leading_symbols () { - # strip the + and - - sed -E "s/^($color_code_regex)[\+\-]/\1 /g" -} - -print_horizontal_rule () { - printf "%$(tput cols)s\n"|tr " " "─" -} - -# run it. -cat $input | format_diff_header | colorize_context_line | strip_leading_symbols - - diff --git a/brew-cask.sh b/brew-cask.sh index 24c2ba5..3f3c02a 100755 --- a/brew-cask.sh +++ b/brew-cask.sh @@ -1,8 +1,8 @@ #!/bin/bash   -# to maintain cask ....  -# brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup`  +# to maintain cask .... +# brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup`   # Install native apps @@ -14,7 +14,7 @@ brew tap caskroom/versions brew cask install spectacle brew cask install dropbox brew cask install gyazo -brew cask install onepassword +brew cask install 1password brew cask install rescuetime brew cask install flux  @@ -47,6 +47,6 @@ brew cask install utorrent  # Not on cask but I want regardless.  -# 3Hub https://itunes.apple.com/us/app/3hub/id427515976?mt=12  +# 3Hub https://itunes.apple.com/us/app/3hub/id427515976?mt=12 # File Multi Tool 5 # Phosphor \ No newline at end of file diff --git a/brew.sh b/brew.sh index 8715a37..c4a663e 100755 --- a/brew.sh +++ b/brew.sh @@ -58,7 +58,9 @@ brew install mtr   # Install other useful binaries -brew install sift +brew install the_silver_searcher +brew install fzf + brew install git brew install imagemagick --with-webp brew install node # This installs `npm` too using the recommended installation method diff --git a/fish/aliases.fish b/fish/aliases.fish index a0fe9b3..fc990f6 100644 --- a/fish/aliases.fish +++ b/fish/aliases.fish @@ -26,17 +26,14 @@ alias brwe=brew #typos alias hosts='sudo $EDITOR /etc/hosts' # yes I occasionally 127.0.0.1 twitter.com ;)   -# `shellswitch [bash|zsh|fish]` -function shellswitch - chsh -s (brew --prefix)/bin/$argv -end - - - # `cat` with beautiful colors. requires Pygments installed. # sudo easy_install -U Pygments alias c='pygmentize -O style=monokai -f console256 -g'  +alias ag='ag -W 200 -f' + +alias diskspace_report="df -P -kHl" +alias free_diskspace_report="diskspace_report"   # Networking. IP address, dig, DNS @@ -54,8 +51,7 @@ alias ungz="gunzip -k" # File size alias fs="stat -f \"%z bytes\""  -# Empty the Trash on all mounted volumes and the main HDD. then clear the useless sleepimage -alias emptytrash="sudo rm -rfv /Volumes/*/.Trashes; rm -rfv ~/.Trash; sudo rm /private/var/vm/sleepimage" +# emptytrash written as a function  # Update installed Ruby gems, Homebrew, npm, and their installed packages alias brew_update="brew -v update; brew -v upgrade --all; brew cleanup; brew cask cleanup; brew prune; brew doctor" diff --git a/fish/config.fish b/fish/config.fish index 9c4bd70..fd19027 100755 --- a/fish/config.fish +++ b/fish/config.fish @@ -2,9 +2,15 @@ set default_user "paulirish" set default_machine "paulirish-macbookair2"   +#set -x DYLD_FALLBACK_LIBRARY_PATH /Users/paulirish/.homebrew/lib + source ~/.config/fish/path.fish source ~/.config/fish/aliases.fish source ~/.config/fish/chpwd.fish +source ~/.config/fish/functions.fish + + +   # Completions @@ -22,8 +28,6 @@ end make_completion g 'git'   - - # Readline colors set -g fish_color_autosuggestion 555 yellow set -g fish_color_command 5f87d7 @@ -83,3 +87,4 @@ set -gx LESS_TERMCAP_so \e'[38;5;246m' # begin standout-mode - info box set -gx LESS_TERMCAP_ue \e'[0m' # end underline set -gx LESS_TERMCAP_us \e'[04;38;5;146m' # begin underline  + diff --git a/fish/functions/fish_prompt.fish b/fish/functions/fish_prompt.fish index 04abe4b..96bfa3e 100755 --- a/fish/functions/fish_prompt.fish +++ b/fish/functions/fish_prompt.fish @@ -13,6 +13,9 @@ function _git_current_branch -d "Output git's current branch name" end ^/dev/null | sed -e 's|^refs/heads/||' end  +function fish_title --description 'Show current path (abbreviated) in iTerm tab title ' + echo (prompt_pwd) +end  function fish_prompt --description 'Write out the prompt'  diff --git a/fish/functions/gr.fish b/fish/functions/gr.fish index 4c2722d..1a0ee1b 100644 --- a/fish/functions/gr.fish +++ b/fish/functions/gr.fish @@ -1,3 +1,12 @@  # git root -alias gr='[ ! -z `git rev-parse --show-cdup` ] && cd `git rev-parse --show-cdup || pwd`' +function gr --description "Jump to the git root" + set -l repo_info (command git rev-parse --git-dir --is-inside-git-dir --is-bare-repository --is-inside-work-tree --short HEAD ^/dev/null) +  test -n "$repo_info"; or return + + set -l cd_up_path (command git rev-parse --show-cdup) + + if test -n $cd_up_path + cd $cd_up_path + end +end diff --git a/fish/functions/ls.fish b/fish/functions/ls.fish index 33c3d8b..fd54db2 100644 --- a/fish/functions/ls.fish +++ b/fish/functions/ls.fish @@ -7,6 +7,8 @@ if begin type gls 1>/dev/null 2>/dev/null or command ls --version 1>/dev/null 2>/dev/null + + set -x CLICOLOR_FORCE 1 end # This is GNU ls function ls --description "List contents of directory" @@ -22,11 +24,11 @@ if begin set param $param --human-readable set param $param --sort=extension set param $param --group-directories-first - if isatty 1 + if isatty 1 set param $param --indicator-style=classify end  - eval "env CLICOLOR_FORCE=1 command $ls $param $argv" + eval $ls $param "$argv" end  if not set -q LS_COLORS diff --git a/fish/path.fish b/fish/path.fish index 6e28fad..aa9d014 100644 --- a/fish/path.fish +++ b/fish/path.fish @@ -13,8 +13,10 @@ for entry in (string split \n $PATH_DIRS) # resolve the {$HOME} substitutions set -l resolved_path (eval echo $entry) if test -d "$resolved_path"; # and not contains $resolved_path $PATH - set -x PA $PA "$resolved_path" + set PA $PA "$resolved_path" end end  -set -x PATH $PA \ No newline at end of file +set PA $PA /Users/paulirish/.rvm/gems/ruby-2.2.1/bin + +set --export PATH $PA \ No newline at end of file diff --git a/setup-a-new-machine.sh b/setup-a-new-machine.sh index 7b5996c..7e60889 100755 --- a/setup-a-new-machine.sh +++ b/setup-a-new-machine.sh @@ -215,5 +215,7 @@ sh .osx # symlink it up! ./symlink-setup.sh  +# add manual symlink for .ssh/config and probably .config/fish + ### ############################################################################################################## ================================================ FILE: test/fixtures/file-moves.diff ================================================ commit a12f64cfa2388b1d07659149db3a77314c9836c8 Author: Scott Baker Date: Thu Feb 25 08:31:54 2016 -0800 Moves diff --git a/appveyor.yml b/appveyor.yml index a6fb95e..43d61c3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -install: +FOOinstall: - git clone --depth 1 https://github.com/sstephenson/bats.git  build: false diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 18a04a3..0000000 --- a/circle.yml +++ /dev/null @@ -1,21 +0,0 @@ -machine: - environment: - - -dependencies: - pre: - - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe"; - - sudo apt-get -y update - - sudo apt-get -y install shellcheck; - - post: - - git clone --depth 1 https://github.com/sstephenson/bats.git - -checkout: - post: - - git submodule update --init - -test: - override: - - bats/bin/bats test/*.bats - - shellcheck diff-so-fancy update-deps.sh diff --git a/hello.txt b/hello.txt new file mode 100644 index 0000000..0c767bc --- /dev/null +++ b/hello.txt @@ -0,0 +1 @@ +HI THERE diff --git a/package.json b/package.json deleted file mode 100644 index c7a1ab2..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "diff-so-fancy", - "version": "0.5.0", - "description": "Good-lookin' diffs with diff-highlight and more", - "bin": { - "diff-so-fancy": "diff-so-fancy", - "diff-highlight": "third_party/diff-highlight/diff-highlight" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/so-fancy/diff-so-fancy.git" - }, - "keywords": [ - "git", - "diff", - "fancy", - "good-lookin'", - "diff-highlight", - "color", - "readable", - "highlight" - ], - "author": "Paul Irish", - "license": "MIT", - "bugs": { - "url": "https://github.com/so-fancy/diff-so-fancy/issues" - }, - "homepage": "https://github.com/so-fancy/diff-so-fancy#readme" -} diff --git a/square.yml b/square.yml new file mode 100644 index 0000000..18a04a3 --- /dev/null +++ b/square.yml @@ -0,0 +1,21 @@ +machine: + environment: + + +dependencies: + pre: + - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-backports restricted main universe"; + - sudo apt-get -y update + - sudo apt-get -y install shellcheck; + + post: + - git clone --depth 1 https://github.com/sstephenson/bats.git + +checkout: + post: + - git submodule update --init + +test: + override: + - bats/bin/bats test/*.bats + - shellcheck diff-so-fancy update-deps.sh ================================================ FILE: test/fixtures/file-perms.diff ================================================ diff --git a/circle.yml b/circle.yml old mode 100644 new mode 100755 diff --git a/foo.json b/foo.json new file mode 100644 index 0000000..316a815 --- /dev/null +++ b/foo.json @@ -0,0 +1,29 @@ +{ + "name": "diff-so-fancy", + "version": "0.5.1", + "description": "Good-lookin' diffs with diff-highlight and more", + "bin": { + "diff-so-fancy": "diff-so-fancy", + "diff-highlight": "third_party/diff-highlight/diff-highlight" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/so-fancy/diff-so-fancy.git" + }, + "keywords": [ + "git", + "diff", + "fancy", + "good-lookin'", + "diff-highlight", + "color", + "readable", + "highlight" + ], + "author": "Paul Irish", + "license": "MIT", + "bugs": { + "url": "https://github.com/so-fancy/diff-so-fancy/issues" + }, + "homepage": "https://github.com/so-fancy/diff-so-fancy#readme" +} diff --git a/package.json b/package.json deleted file mode 100644 index 316a815..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "diff-so-fancy", - "version": "0.5.1", - "description": "Good-lookin' diffs with diff-highlight and more", - "bin": { - "diff-so-fancy": "diff-so-fancy", - "diff-highlight": "third_party/diff-highlight/diff-highlight" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/so-fancy/diff-so-fancy.git" - }, - "keywords": [ - "git", - "diff", - "fancy", - "good-lookin'", - "diff-highlight", - "color", - "readable", - "highlight" - ], - "author": "Paul Irish", - "license": "MIT", - "bugs": { - "url": "https://github.com/so-fancy/diff-so-fancy/issues" - }, - "homepage": "https://github.com/so-fancy/diff-so-fancy#readme" -} ================================================ 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 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 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 +#include 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 ================================================ diff --git a/test.txt b/test.txt index 5836369..51ccf71 100644 --- a/test.txt +++ b/test.txt @@ -1,6 +1 @@ -é - +H cam h saw he conqurd ================================================ 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 ================================================ diff --git a/fish/functions/ls.fish b/fish/functions/ls.fish index 33c3d8b..fd54db2 100644 --- a/fish/functions/ls.fish +++ b/fish/functions/ls.fish @@ -7,6 +7,8 @@ if begin type gls 1>/dev/null 2>/dev/null or command ls --version 1>/dev/null 2>/dev/null + + set -x CLICOLOR_FORCE 1 end # This is GNU ls function ls --description "List contents of directory" @@ -22,11 +24,11 @@ if begin set param $param --human-readable set param $param --sort=extension set param $param --group-directories-first - if isatty 1 + if isatty 1 set param $param --indicator-style=classify end  - eval "env CLICOLOR_FORCE=1 command $ls $param $argv" + eval $ls $param "$argv" end - if not set -q LS_COLORS ================================================ FILE: test/fixtures/mnemonicprefix.diff ================================================ diff --git i/diff-so-fancy w/diff-so-fancy index 2323d9b..a280985 100755 --- i/diff-so-fancy +++ w/diff-so-fancy @@ -48,10 +48,6 @@ colorize_context_line () { $SED -E "s/@@$reset_color $reset_color(.*$)/@@ $dim_magenta\1/g" }  -mark_empty_lines () { - $SED -E "s/^$color_code_regex[\+\-]$reset_color\s*$/$invert_color\1 $reset_escape/g" -} - strip_leading_symbols () { # strip the + and - $SED -E "s/^($color_code_regex)[\+\-]/\1 /g" @@ -83,7 +79,6 @@ cat $input \ | $diff_highlight \ | format_diff_header \ | colorize_context_line \ - | mark_empty_lines \ | strip_leading_symbols \ | strip_first_column \ | print_header_clean diff --git i/libs/header_clean/header_clean.pl w/libs/header_clean/header_clean.pl index 23df5e7..54b3da1 100755 --- i/libs/header_clean/header_clean.pl +++ w/libs/header_clean/header_clean.pl @@ -9,6 +9,8 @@ my $remove_file_delete_header = 1; my $clean_permission_changes = 1; my $change_hunk_indicators = 1;  +use Data::Dump::Color; + #################################################################################  my $ansi_sequence_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,3})[mK])?/; @@ -29,12 +31,12 @@ for (my $i = 0; $i <= $#input; $i++) { ######################################## # Find the first file: --- a/README.md # ######################################## - } elsif ($line =~ /^$ansi_sequence_regex--- (a\/)?(.+?)(\e|$)/) { + } elsif ($line =~ /^$ansi_sequence_regex--- ([abiwco]\/)?(.+?)(\e|$)/) { $file_1 = $5;  # Find the second file on the next line: +++ b/README.md my $next = $input[++$i]; - $next =~ /^$ansi_sequence_regex\+\+\+ (b\/)?(.+?)(\e|$)/; + $next =~ /^$ansi_sequence_regex\+\+\+ ([abiwco]\/)?(.+?)(\e|$)/; if ($1) { print $1; # Print out whatever color we're using } diff --git i/test/header_clean.bats w/test/header_clean.bats index 4a3e7ee..a8385a5 100644 --- i/test/header_clean.bats +++ w/test/header_clean.bats @@ -50,3 +50,8 @@ output=$( load_fixture "file-moves" | $diff_so_fancy ) assert_output --partial '@ setup-a-new-machine.sh:33 @' assert_output --partial '@ setup-a-new-machine.sh:218 @' } + +@test "mnemonicprefix" { + output=$( load_fixture "mnemonicprefix" | $diff_so_fancy ) + assert_output --partial '@ setup-a-new-machine.sh:33 @' +} diff --git c/hello.txt i/hello.txt deleted file mode 100644 index 0c767bc..0000000 --- c/hello.txt +++ /dev/null @@ -1 +0,0 @@ -HI THERE ================================================ 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 ================================================ diff --git setup-a-new-machine.sh setup-a-new-machine.sh index 7b5996c..67eec2a 100755 --- setup-a-new-machine.sh +++ setup-a-new-machine.sh @@ -30,6 +30,7 @@ cp -R ~/.gnupg ~/migration/home cp /Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist ~/migration # wifi  cp ~/Library/Preferences/net.limechat.LimeChat.plist ~/migration +cp ~/Library/Preferences/com.tinyspeck.slackmacgap.plist ~/migration  cp -R ~/Library/Services ~/migration # automator stuff  @@ -215,5 +216,7 @@ sh .osx # symlink it up! ./symlink-setup.sh  +# add manual symlink for .ssh/config and probably .config/fish + ### ############################################################################################################## ================================================ 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('/') +@app.route(f'/{SECTION}/') 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('/') +@app.route(f'/{SECTION}/') 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 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 ================================================ diff --git package.json package.json index 97965ab..f3ce90a 100644 --- package.json +++ package.json @@ -13,8 +13,8 @@ "url": "git+https://github.com/so-fancy/diff-so-fancy.git" }, "keywords": [ - "git", "diff", + "git", "fancy", "good-lookin'", "diff-highlight", ================================================ FILE: test/fixtures/unicode.diff ================================================ diff --git a/unicodes b/unicodes index 223f57d..1c2066d 100644 --- a/unicodes +++ b/unicodes @@ -1 +1 @@ -aao +åäöç ================================================ 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 @@ -aattach a file to a message -bspecify a blind carbon-copy (BCC) address -cspecify a carbon-copy (Cc) address --dlog 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) +-dlog 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) -Dprint the value of all Mutt variables to stdout -Eedit the draft (-H) or include (-i) file -especify a config command to be run after initialization files are read ================================================ 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}" < 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}" < \$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 () { $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