URI: 
       tindent - gitzone - git-based zone management tool for static and dynamic domains
  HTML git clone https://git.parazyd.org/gitzone
   DIR Log
   DIR Files
   DIR Refs
       ---
   DIR commit 24a356f140401c265ef5b65a3edf3b617e56fad8
   DIR parent 975d717bb8204ccbdbb2243df471849b9c6e9b6e
  HTML Author: tg(x) <*@tg-x.net>
       Date:   Sat,  1 Jun 2013 07:13:15 +0200
       
       indent
       
       Diffstat:
         M bin/gitzone                         |     496 ++++++++++++++++----------------
         M bin/gitzone-shell                   |      42 ++++++++++++++++----------------
       
       2 files changed, 269 insertions(+), 269 deletions(-)
       ---
   DIR diff --git a/bin/gitzone b/bin/gitzone
       t@@ -67,330 +67,330 @@ $SIG{__DIE__} = \&cleanup;
        cleanup;
        
        sub git {
       -  my ($args, $print, $ret) = @_;
       -  $ret ||=0;
       -  print "% git $args\n" if $verbosity >= 2;
       -  $_ = `$git $args 2>&1`;
       -  $print = 1 if !defined $print && $verbosity >= 1;
       -  if ($print) {
       -    #my $cwd = cwd; s/$cwd//g; # print relative paths
       -    print;
       -  }
       -  if ($ret >= 0 && $? >> 8 != $ret) {
       -    my ($package, $filename, $line) = caller;
       -    print;
       -    die "Died at line $line.\n";
       -  }
       -  return $_;
       +    my ($args, $print, $ret) = @_;
       +    $ret ||=0;
       +    print "% git $args\n" if $verbosity >= 2;
       +    $_ = `$git $args 2>&1`;
       +    $print = 1 if !defined $print && $verbosity >= 1;
       +    if ($print) {
       +        #my $cwd = cwd; s/$cwd//g; # print relative paths
       +        print;
       +    }
       +    if ($ret >= 0 && $? >> 8 != $ret) {
       +        my ($package, $filename, $line) = caller;
       +        print;
       +        die "Died at line $line.\n";
       +    }
       +    return $_;
        }
        
        # Load BIND config files specified in the $repos config variable.
        # First load the -default key, then the $repo key.
        sub load_repo_config {
       -  my $key = shift || '-default';
       -
       -  # move files not in a dir to a . dir for easier processing
       -  for my $file (keys %{$repos->{$key}}) {
       -    next if ref $repos->{$key}->{$file} eq 'HASH';
       -    $repos->{$key}->{'.'}->{$file} = $repos->{$key}->{$file};
       -    delete $repos->{$key}->{$file};
       -  }
       -
       -  for my $dir (keys %{$repos->{$key}}) {
       -    my $d = $repos->{$key}->{$dir};
       -    for my $file (keys %$d) {
       -      $d->{$file} = $default_view if $d->{$file} eq 1;
       -      $d->{$file} = [$d->{$file}] if ref $d->{$file} ne 'ARRAY';
       -      next unless $file =~ m,^/,;
       -      if (-f $file) {
       -        open FILE, '<', $file or die $!;
       -        while (<FILE>) {
       -          if (/^\s*zone\s+"([^"]+)"/) {
       -            $repos->{$repo}->{$dir}->{$1} = $d->{$file};
       -          }
       -        }
       -        close FILE;
       -      }
       -      delete $d->{$file} if $key ne '-default';
       +    my $key = shift || '-default';
       +
       +    # move files not in a dir to a . dir for easier processing
       +    for my $file (keys %{$repos->{$key}}) {
       +        next if ref $repos->{$key}->{$file} eq 'HASH';
       +        $repos->{$key}->{'.'}->{$file} = $repos->{$key}->{$file};
       +        delete $repos->{$key}->{$file};
       +    }
       +
       +    for my $dir (keys %{$repos->{$key}}) {
       +        my $d = $repos->{$key}->{$dir};
       +        for my $file (keys %$d) {
       +            $d->{$file} = $default_view if $d->{$file} eq 1;
       +            $d->{$file} = [$d->{$file}] if ref $d->{$file} ne 'ARRAY';
       +            next unless $file =~ m,^/,;
       +            if (-f $file) {
       +                open FILE, '<', $file or die $!;
       +                while (<FILE>) {
       +                    if (/^\s*zone\s+"([^"]+)"/) {
       +                        $repos->{$repo}->{$dir}->{$1} = $d->{$file};
       +                    }
       +                }
       +                close FILE;
       +            }
       +            delete $d->{$file} if $key ne '-default';
       +        }
            }
       -  }
        
       -  load_repo_config($repo) if $key eq '-default';
       +    load_repo_config($repo) if $key eq '-default';
        }
        
        sub check_what_changed {
       -  my ($old, $new) = @_;
       +    my ($old, $new) = @_;
        
       -  # diff with empty tree if there's no previous commit
       -  $old = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' if !$old || $old =~ /^0+$/;
       +    # diff with empty tree if there's no previous commit
       +    $old = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' if !$old || $old =~ /^0+$/;
        
       -  $_ = git "diff --raw ". ($new ? "$old..$new" : $old);
       +    $_ = git "diff --raw ". ($new ? "$old..$new" : $old);
        
       -  # parse diff output, add only valid zone names to %files for parsing
       -  $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([a-z0-9./-]+)$,gm;
       +    # parse diff output, add only valid zone names to %files for parsing
       +    $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([a-z0-9./-]+)$,gm;
        }
        
        sub process_files {
       -  $files{$_} = 0 for @_;
       -  process_file($_) for keys %files;
       -  check_zones();
       -
       -  if (@changed_files) {
       -    print "adding changed files: @changed_files\n" if $verbosity >= 2;
       -    git "add @changed_files";
       -  }
       +    $files{$_} = 0 for @_;
       +    process_file($_) for keys %files;
       +    check_zones();
       +
       +    if (@changed_files) {
       +        print "adding changed files: @changed_files\n" if $verbosity >= 2;
       +        git "add @changed_files";
       +    }
        }
        
        sub process_file {
       -  my ($file, $depth) = @_;
       -  my (@newfile, $changed, @inc_by);
       -  print ">> process_file($file)\n" if $verbosity >= 3;
       -
       -  return 0 if $files{$file}; # already processed
       -  return -1 unless -f $file;
       -
       -  print ">>> processing $file\n" if $verbosity >= 3;
       -  $files{$_}++;
       -
       -  open FILE, '<', $file or die $!;
       -  my $n = 0;
       -  while (<FILE>) {
       -    $n++;
       -    my $line = $_;
       -    if (/^(.*)(\b\d+\b)(.*?;AUTO_INCREMENT\b.*)$/) {
       -      # increment serial where marked with ;AUTO_INCREMENT
       -      # if length of serial is 10 and starts with 20 treat it as a date
       -      my ($a,$s,$z) = ($1,int $2,$3);
       -      $date ||= strftime '%Y%m%d', localtime;
       -      $s = ($s =~ /^$date/ || $s < 2000000000 || $s >= 2100000000) ? $s + 1 : $date.'00';
       -      $line = "$a$s$z\n";
       -      $changed = 1;
       -    } elsif (/^(\s*\$INCLUDE\s+)(\S+)(.*)$/) {
       -      my ($a,$inc_file,$z) = ($1,$2,$3);
       -      unless ($unrestricted_includes) {
       -        # check $INCLUDE lines for files outside the repo dir
       -        unless ($inc_file =~ m,^$repo/, && $inc_file !~ /\.\./) {
       -          close FILE;
       -          die "Error in $file:$n: invalid included file name, it should start with: $repo/\n";
       -        }
       -      }
       -    } else {
       -      if ($n == 1 && /^;INCLUDED_BY\s+(.*)$/) {
       -        @inc_by = split /\s+/, $1;
       -      }
       +    my ($file, $depth) = @_;
       +    my (@newfile, $changed, @inc_by);
       +    print ">> process_file($file)\n" if $verbosity >= 3;
       +
       +    return 0 if $files{$file}; # already processed
       +    return -1 unless -f $file;
       +
       +    print ">>> processing $file\n" if $verbosity >= 3;
       +    $files{$_}++;
       +
       +    open FILE, '<', $file or die $!;
       +    my $n = 0;
       +    while (<FILE>) {
       +        $n++;
       +        my $line = $_;
       +        if (/^(.*)(\b\d+\b)(.*?;AUTO_INCREMENT\b.*)$/) {
       +            # increment serial where marked with ;AUTO_INCREMENT
       +            # if length of serial is 10 and starts with 20 treat it as a date
       +            my ($a,$s,$z) = ($1,int $2,$3);
       +            $date ||= strftime '%Y%m%d', localtime;
       +            $s = ($s =~ /^$date/ || $s < 2000000000 || $s >= 2100000000) ? $s + 1 : $date.'00';
       +            $line = "$a$s$z\n";
       +            $changed = 1;
       +        } elsif (/^(\s*\$INCLUDE\s+)(\S+)(.*)$/) {
       +            my ($a,$inc_file,$z) = ($1,$2,$3);
       +            unless ($unrestricted_includes) {
       +                # check $INCLUDE lines for files outside the repo dir
       +                unless ($inc_file =~ m,^$repo/, && $inc_file !~ /\.\./) {
       +                    close FILE;
       +                    die "Error in $file:$n: invalid included file name, it should start with: $repo/\n";
       +                }
       +            }
       +        } else {
       +            if ($n == 1 && /^;INCLUDED_BY\s+(.*)$/) {
       +                @inc_by = split /\s+/, $1;
       +            }
       +        }
       +        push @newfile, $line;
            }
       -    push @newfile, $line;
       -  }
       -  close FILE;
       +    close FILE;
        
       -  if ($changed) {
       -    print ">>> $file changed, saving\n" if $verbosity >= 3;
       +    if ($changed) {
       +        print ">>> $file changed, saving\n" if $verbosity >= 3;
        
       -    open FILE, '>', $file or die $!;
       -    print FILE for @newfile;
       -    close FILE;
       +        open FILE, '>', $file or die $!;
       +        print FILE for @newfile;
       +        close FILE;
        
       -    push @changed_files, $file;
       -  }
       +        push @changed_files, $file;
       +    }
        
       -  if ($depth++ < $max_depth) {
       -    process_file($_, $depth) for @inc_by;
       -  } else {
       -    print "Warning: ;INCLUDED_BY is followed only up to $max_depth levels,\n".
       -          "  the following files are not reloaded: @inc_by\n";
       -  }
       +    if ($depth++ < $max_depth) {
       +        process_file($_, $depth) for @inc_by;
       +    } else {
       +        print "Warning: ;INCLUDED_BY is followed only up to $max_depth levels,\n".
       +            "  the following files are not reloaded: @inc_by\n";
       +    }
        
       -  return 1;
       +    return 1;
        }
        
        sub check_zones {
       -  print ">> check_zones: ,",%files,"\n" if $verbosity >= 3;
       -  for my $file (keys %files) {
       -    my ($zone, $dir) = fileparse $file;
       -    $zone =~ s/\.signed$//;
       -    $dir = substr $dir, 0, -1;
       -    # skip files with errors and those that are not in the config
       -    next unless $files{$file} > 0 && exists $repos->{$repo}->{$dir}->{$zone};
       -
       -    print "Checking zone $zone\n";
       -    print `$named_checkzone -w .. '$zone' '$repo/$file'`;
       -    clean_exit 1 if $?; # error, reject push
       -    push @zones, $file;
       -  }
       +    print ">> check_zones: ,",%files,"\n" if $verbosity >= 3;
       +    for my $file (keys %files) {
       +        my ($zone, $dir) = fileparse $file;
       +        $zone =~ s/\.signed$//;
       +        $dir = substr $dir, 0, -1;
       +        # skip files with errors and those that are not in the config
       +        next unless $files{$file} > 0 && exists $repos->{$repo}->{$dir}->{$zone};
       +
       +        print "Checking zone $zone\n";
       +        print `$named_checkzone -w .. '$zone' '$repo/$file'`;
       +        clean_exit 1 if $?; # error, reject push
       +        push @zones, $file;
       +    }
        }
        
        sub save_list_file {
       -  if (@zones) {
       -    print "Zone check passed: @zones\n";
       -    # save changed zone list for post-receive hook
       -    open FILE, '>>', $list_file or die $!;
       -    print FILE join(' ', @zones), "\n";
       -    close FILE;
       -  } else {
       -    print "No zones to reload\n";
       -  }
       +    if (@zones) {
       +        print "Zone check passed: @zones\n";
       +        # save changed zone list for post-receive hook
       +        open FILE, '>>', $list_file or die $!;
       +        print FILE join(' ', @zones), "\n";
       +        close FILE;
       +    } else {
       +        print "No zones to reload\n";
       +    }
        }
        
        sub load_list_file {
       -  return unless -f $list_file;
       -  my %zones;
       -  open FILE, '<', $list_file or die $!;
       -  while (<FILE>) {
       -    $zones{$_} = 1 for split /[\s\n\r]+/;
       -  }
       -  close FILE;
       -  @zones = keys %zones;
       +    return unless -f $list_file;
       +    my %zones;
       +    open FILE, '<', $list_file or die $!;
       +    while (<FILE>) {
       +        $zones{$_} = 1 for split /[\s\n\r]+/;
       +    }
       +    close FILE;
       +    @zones = keys %zones;
        }
        
        sub install_zones {
       -  print "Reloading changed zones: @zones\n";
       +    print "Reloading changed zones: @zones\n";
        
       -  my $cwd = cwd;
       +    my $cwd = cwd;
        
       -  chdir "$zone_dir/$repo" or die $!;
       -  git "clone $cwd ." unless -d '.git';
       -  git 'fetch';
       -  git 'reset --hard remotes/origin/master';
       +    chdir "$zone_dir/$repo" or die $!;
       +    git "clone $cwd ." unless -d '.git';
       +    git 'fetch';
       +    git 'reset --hard remotes/origin/master';
        
       -  for my $file (@zones) {
       -    my ($zone, $dir) = fileparse $file;
       -    $zone =~ s/\.signed$//;
       -    $dir = substr $dir, 0, -1;
       -    my $view = $repos->{$repo}->{$dir}->{$zone};
       -    print "$_/$zone: ", `$rndc reload '$zone' $class $_` for @$view;
       -  }
       +    for my $file (@zones) {
       +        my ($zone, $dir) = fileparse $file;
       +        $zone =~ s/\.signed$//;
       +        $dir = substr $dir, 0, -1;
       +        my $view = $repos->{$repo}->{$dir}->{$zone};
       +        print "$_/$zone: ", `$rndc reload '$zone' $class $_` for @$view;
       +    }
        
       -  unlink $list_file;
       +    unlink $list_file;
        }
        
        # save working dir state
        # (git stash wouldn't work without conflicts if there's a
        # change in both the index & working tree in the same file)
        sub stash_save {
       -  $stash_file = File::Temp::tempnam('.git', '.gitzone-stash-');
       -  print "Saving working tree to $stash_file\n";
       -  git "update-index --refresh -q", 0, -1;
       -  git "diff >$stash_file";
       -  git 'checkout .';
       +    $stash_file = File::Temp::tempnam('.git', '.gitzone-stash-');
       +    print "Saving working tree to $stash_file\n";
       +    git "update-index --refresh -q", 0, -1;
       +    git "diff >$stash_file";
       +    git 'checkout .';
        }
        
        # restore working dir
        sub stash_pop {
       -  print "Restoring working tree from $stash_file\n";
       -  git "apply --reject --whitespace=nowarn $stash_file", 1, -1;
       -  unlink $stash_file unless $?;
       +    print "Restoring working tree from $stash_file\n";
       +    git "apply --reject --whitespace=nowarn $stash_file", 1, -1;
       +    unlink $stash_file unless $?;
        }
        
        sub pre_receive {
       -  my ($old, $new, $ref);
       -
       -  while (<STDIN>) { # <old-value> SP <new-value> SP <ref-name> LF
       -    print if $verbosity >= 1;
       -    next unless m,(\w+) (\w+) ([\w/]+),;
       -    next if $3 ne 'refs/heads/master'; # only process master branch
       -    die "Denied branch 'new', choose another name\n" if $3 eq 'refs/head/new';
       -    ($old, $new, $ref) = ($1, $2, $3);
       -  }
       -
       -  # nothing for master branch, exit
       -  clean_exit 0 unless $ref;
       -
       -  # checkout changes
       -  git "checkout -qf $new";
       -  check_what_changed($old, $new);
       -  load_repo_config;
       -  process_files;
       -  git "commit -nm 'auto increment: @changed_files'", 1 if @changed_files;
       -  save_list_file;
       -
       -  # save new commits in a new branch
       -  git 'checkout -B new';
       +    my ($old, $new, $ref);
       +
       +    while (<STDIN>) { # <old-value> SP <new-value> SP <ref-name> LF
       +        print if $verbosity >= 1;
       +        next unless m,(\w+) (\w+) ([\w/]+),;
       +        next if $3 ne 'refs/heads/master'; # only process master branch
       +        die "Denied branch 'new', choose another name\n" if $3 eq 'refs/head/new';
       +        ($old, $new, $ref) = ($1, $2, $3);
       +    }
       +
       +    # nothing for master branch, exit
       +    clean_exit 0 unless $ref;
       +
       +    # checkout changes
       +    git "checkout -qf $new";
       +    check_what_changed($old, $new);
       +    load_repo_config;
       +    process_files;
       +    git "commit -nm 'auto increment: @changed_files'", 1 if @changed_files;
       +    save_list_file;
       +
       +    # save new commits in a new branch
       +    git 'checkout -B new';
        }
        
        sub pre_commit {
       -  stash_save;
       +    stash_save;
        
       -  $cleanup = sub {
       -    # reset any changes, e.g. auto inc.
       -    git 'checkout .';
       -    stash_pop;
       -  };
       +    $cleanup = sub {
       +        # reset any changes, e.g. auto inc.
       +        git 'checkout .';
       +        stash_pop;
       +    };
        
       -  git 'rev-parse --verify HEAD', 0, -1;
       -  check_what_changed($? ? undef : 'HEAD');
       -  load_repo_config;
       -  process_files;
       +    git 'rev-parse --verify HEAD', 0, -1;
       +    check_what_changed($? ? undef : 'HEAD');
       +    load_repo_config;
       +    process_files;
        
       -  $cleanup = sub {
       -    stash_pop;
       -  };
       +    $cleanup = sub {
       +        stash_pop;
       +    };
        
       -  save_list_file;
       +    save_list_file;
        }
        
        sub post_receive {
       -  print "\n";
       +    print "\n";
        
       -  # move master to new
       -  git 'checkout -f master';
       -  git 'reset --hard new';
       +    # move master to new
       +    git 'checkout -f master';
       +    git 'reset --hard new';
        
       -  load_repo_config;
       -  load_list_file;
       -  install_zones;
       +    load_repo_config;
       +    load_list_file;
       +    install_zones;
        
       -  print "Done. Don't forget to pull if you use auto increment.\n";
       +    print "Done. Don't forget to pull if you use auto increment.\n";
        }
        
        sub post_commit {
       -  print "\n";
       +    print "\n";
        
       -  load_repo_config;
       -  load_list_file;
       -  install_zones;
       -  print "Done.\n";
       +    load_repo_config;
       +    load_list_file;
       +    install_zones;
       +    print "Done.\n";
        }
        
        sub update_record {
       -  my ($c, $file, @record) = split /\s+/, shift;
       -  my ($ip) = $ENV{SSH_CLIENT} =~ /^([\d.]+|[a-f\d:]+)\s/i or die "Invalid IP address\n";
       -  my $re = qr/^\s*/i;
       -  $re = qr/$re$_\s+/i for (@record);
       -  my $matched = 0;
       -  my $changed = 0;
       -  my @newfile;
       -
       -  git 'checkout -f master';
       -
       -  open FILE, '<', $file or die "$file: $!";
       -  while (<FILE>) {
       -    my $line = $_;
       -    if (!$matched && s/($re)([\d.]+|[a-f\d:]+)/$1$ip/i) {
       -      print "Matched record:\n$line";
       -      $matched = 1;
       -      if ($line ne "$1$ip\n") {
       -        $changed = 1;
       -        $line = "$1$ip\n";
       -        print "Updating it with:\n$line";
       -      } else {
       -        print "Not updating: already up-to-date\n";
       -        close FILE;
       -        clean_exit 0;
       -      }
       +    my ($c, $file, @record) = split /\s+/, shift;
       +    my ($ip) = $ENV{SSH_CLIENT} =~ /^([\d.]+|[a-f\d:]+)\s/i or die "Invalid IP address\n";
       +    my $re = qr/^\s*/i;
       +    $re = qr/$re$_\s+/i for (@record);
       +    my $matched = 0;
       +    my $changed = 0;
       +    my @newfile;
       +
       +    git 'checkout -f master';
       +
       +    open FILE, '<', $file or die "$file: $!";
       +    while (<FILE>) {
       +        my $line = $_;
       +        if (!$matched && s/($re)([\d.]+|[a-f\d:]+)/$1$ip/i) {
       +            print "Matched record:\n$line";
       +            $matched = 1;
       +            if ($line ne "$1$ip\n") {
       +                $changed = 1;
       +                $line = "$1$ip\n";
       +                print "Updating it with:\n$line";
       +            } else {
       +                print "Not updating: already up-to-date\n";
       +                close FILE;
       +                clean_exit 0;
       +            }
       +        }
       +        push @newfile, $line;
            }
       -    push @newfile, $line;
       -  }
       -  close FILE;
       -  die "No matching record in $file: @record\n" unless $matched;
       +    close FILE;
       +    die "No matching record in $file: @record\n" unless $matched;
        
       -  open FILE, '>', $file or die $!;
       -  print FILE for @newfile;
       -  close FILE;
       +    open FILE, '>', $file or die $!;
       +    print FILE for @newfile;
       +    close FILE;
        
       -  git "commit -nm 'update-record: $file' '$file'", 1;
       +    git "commit -nm 'update-record: $file' '$file'", 1;
        
       -  load_repo_config;
       -  process_files $file;
       -  git "commit -nm 'auto increment: @changed_files'", 1 if @changed_files;
       -  install_zones if @zones;
       +    load_repo_config;
       +    process_files $file;
       +    git "commit -nm 'auto increment: @changed_files'", 1 if @changed_files;
       +    install_zones if @zones;
        }
   DIR diff --git a/bin/gitzone-shell b/bin/gitzone-shell
       t@@ -34,35 +34,35 @@ git=/usr/bin/git
        grep=/bin/grep
        
        function error {
       -  echo "fatal: What do you think I am? A shell?"
       -  exit 128
       +    echo "fatal: What do you think I am? A shell?"
       +    exit 128
        }
        
        if [ "$1" != "-c" ]; then error; fi
        cmd=$2
        
        if [[ "$cmd" == git-upload-pack* ]]; then
       -  $git upload-pack $repo_dir/$repo
       +    $git upload-pack $repo_dir/$repo
        elif [[ "$cmd" == git-receive-pack* ]]; then
       -  $git receive-pack $repo_dir/$repo
       +    $git receive-pack $repo_dir/$repo
        elif [[ "$cmd" == update-record* ]]; then
       -  cd $repo_dir/$repo/.git
       -  $gitzone $config update-record "$cmd"
       +    cd $repo_dir/$repo/.git
       +    $gitzone $config update-record "$cmd"
        elif [ -f $allow_key_mgmt_file ]; then
       -  if [ "$cmd" == list-keys ]; then
       -    cat .ssh/authorized_keys
       -  elif [[ "$cmd" == add-key* ]]; then
       -    key="${cmd:8}"
       -    echo "$key" >> .ssh/authorized_keys && \
       -      echo "key added"
       -  elif [[ "$cmd" == del-key* ]]; then
       -    key="${cmd:8}"
       -    $grep -v "$key" .ssh/authorized_keys > .ssh/authorized_keys-new && \
       -      mv .ssh/authorized_keys-new .ssh/authorized_keys && \
       -      echo "key deleted"
       -  else
       -    error
       -  fi
       +    if [ "$cmd" == list-keys ]; then
       +        cat .ssh/authorized_keys
       +    elif [[ "$cmd" == add-key* ]]; then
       +        key="${cmd:8}"
       +        echo "$key" >> .ssh/authorized_keys && \
       +            echo "key added"
       +    elif [[ "$cmd" == del-key* ]]; then
       +        key="${cmd:8}"
       +        $grep -v "$key" .ssh/authorized_keys > .ssh/authorized_keys-new && \
       +            mv .ssh/authorized_keys-new .ssh/authorized_keys && \
       +            echo "key deleted"
       +    else
       +        error
       +    fi
        else
       -  error
       +    error
        fi