#!/usr/bin/perl
# $NHDT-Date: 1524689646 2018/04/25 20:54:06 $ Branch: NetHack-3.7 $:$NHDT-Revision: 1.7 $
# Copyright (c) 2015 by Kenneth Lorber, Kensington, Maryland
# NetHack may be freely redistributed.  See license for details.

# Note: was originally called nhdate; the rename is not reflected in the code.

use strict;
our %opt;	#cmd v n f F m (other single char, but we don't care)
my $mode;	# a c d f (add, commit, date, date -f)
our $count;
our $skip;

if(length $ENV{GIT_PREFIX}){
    chdir($ENV{GIT_PREFIX}) or die "Can't chdir $ENV{GIT_PREFIX}: $!";
}

#SO how do we know if a file has changed?
#(git status: git status --porcelain --ignored -- FILES.
#maybe + -z but it's a question of rename operations - probably doesn't
# matter, but need to experiment.

# key:  [dacf] first character of opt{cmd} (f if nhsub -f or add -f)
#	first 2 chars of "git status --porcelain --ignored"
# (see "git help status" for table)
# No default.  Undef means something unexpected happened.
my %codes = (
	    #           [MD]   not updated
    'f M'=>1, 'f D'=>1,
    'a M'=>0, 'a D'=>0,
    'd M'=>0, 'd D'=>0,
    'c M'=>0, 'c D'=>0,

    'dM '=>0, 'dMM'=>1, 'dMD'=>0,
    'aM '=>0, 'aMM'=>1, 'aMD'=>0,
    'cM '=>0, 'cMM'=>1, 'cMD'=>0,
    'fM '=>0, 'fMM'=>1, 'fMD'=>0,
	    #           M        [ MD]   updated in index

    'dA '=>1, 'dAM'=>1, 'dAD'=>1,
    'aA '=>1, 'aAM'=>1, 'aAD'=>1,
    'cA '=>1, 'cAM'=>1, 'cAD'=>1,
    'fA '=>1, 'fAM'=>1, 'fAD'=>1,
	   #		A        [ MD]   added to index

    'dD '=>0, 'dDM'=>0,
    'aD '=>1, 'aDM'=>1,
    'cD '=>0, 'cDM'=>0,
    'fD '=>1, 'fDM'=>1,
	   # D         [ M]   deleted from index

    'dR '=>0, 'dRM'=>1, 'dRD'=>0,
    'aR '=>0, 'aRM'=>1, 'aRD'=>0,
    'cR '=>0, 'cRM'=>1, 'cRD'=>0,
    'fR '=>0, 'fRM'=>1, 'fRD'=>0,
	    #           R        [ MD]   renamed in index

    'dC '=>0, 'dCM'=>1, 'dCD'=>0,
    'aC '=>0, 'aCM'=>1, 'aCD'=>0,
    'cC '=>0, 'cCM'=>1, 'cCD'=>0,
    'fC '=>0, 'fCM'=>1, 'fCD'=>0,
	    #           C        [ MD]   copied in index

    'aM '=>1, 'aA '=>1, 'aR '=>1, 'aC '=>1,
    'fM '=>1, 'fA '=>1, 'fR '=>1, 'fC '=>1,
	   # [MARC]          index and work tree matches

    'd M'=>1, 'dMM'=>1, 'dAM'=>1, 'dRM'=>1, 'dCM'=>1,
    'a M'=>1, 'aMM'=>1, 'aAM'=>1, 'aRM'=>1, 'aCM'=>1,
    'c M'=>1, 'cMM'=>1, 'cAM'=>1, 'cRM'=>1, 'cCM'=>1,
    'f M'=>1, 'fMM'=>1, 'fAM'=>1, 'fRM'=>1, 'fCM'=>1,
	   # [ MARC]     M work tree changed since index

    'd D'=>0, 'dMD'=>0, 'dAD'=>0, 'dRD'=>0, 'dCD'=>0,
    'a D'=>0, 'aMD'=>0, 'aAD'=>0, 'aRD'=>0, 'aCD'=>0,
    'c D'=>0, 'cMD'=>0, 'cAD'=>0, 'cRD'=>0, 'cCD'=>0,
    'f D'=>0, 'fMD'=>0, 'fAD'=>0, 'fRD'=>0, 'fCD'=>0,
	   # [ MARC]     D    deleted in work tree

        # -------------------------------------------------
        # DD    unmerged, both deleted
        # AU    unmerged, added by us
        # UD    unmerged, deleted by them
        # UA    unmerged, added by them
        # DU    unmerged, deleted by us
        # AA    unmerged, both added
        # UU    unmerged, both modified
	# -------------------------------------------------
	    # ??    untracked
    'a??'=>1, 'f??'=>1,
    'd??'=>0, 'c??'=>0,

	    # !!    ignored
    'f!!'=>1,

    'a!!'=>0, 'd!!'=>0, 'c!!'=>0,

    'f@@'=>1,			# @@    internal ignored
    'a@@'=>0, 'd@@'=>0, 'c@@'=>0
);

# OS hackery
my $PDS = '/';
if ($^O eq "MSWin32")
{
    $PDS = '\\';
}

# various command line options to consider and what the code actually does:
#DONE nhcommit with no files should exit(0)
#DONE nhadd with no files should exit(0)
#DONE commit -a?
#	add root dir
#DONE commit -a + files -> exit(0)
#nothing: commit --interactive/--patch
#nothing: add -i/--interactive --patch/-p?
#nothing: add -u/--update?????? -A/--all/--no-ignore-removal???
#nothing (not quite right): add --no-all --ignore-removal???
#DONE add --refresh
#nothing: add -N/--intent-to-add
#DONE add -n - exit(0)
#DONE add --dry-run - exit 0
#DONE commit --dry-run - exit 0
#DONE?: add foo/\*/x (letting git expand the filenames)

my @rawlist0 = &cmdparse(@ARGV);

# Use git ls-files to expand command line filepaths with wildcards.
# Let's try this for all commands.
my @rawlist;
foreach my $e (@rawlist0){
    if($e =~ m/[?*[\\]/){
	my @rv = &lsfiles(undef, $e);
	push(@rawlist, @rv) if(@rv);
	if($opt{f}){
	    my @rv = &lsfiles('-i', $e);
	    push(@rawlist, @rv) if(@rv);
	}
    } else {
	push(@rawlist, $e);
    }
}

push(@rawlist,'.') if($#rawlist == -1);

# pick up the prefix for substitutions in this repo
my $PREFIX = &git_config('nethack','substprefix');
die "nethack.substprefix not set in git config" unless(length($PREFIX) > 0);
print "PREFIX: '$PREFIX'\n" if($opt{v});

while(@rawlist){
    my $raw = shift @rawlist;
    if(-f $raw){
		&schedule_work($raw);
		next;
    }
    if(-d $raw){
	if($raw =~ m!$PDS.git$!o){
		print "SKIP $raw\n" if($opt{v}>=2);
		next;
	}
	opendir RDIR,$raw or die "Can't opendir: $raw";
	local($_);	# needed until perl 5.11.2
	while($_ = readdir RDIR){
	    next if(m/^\.\.?$/);
	    if(m/^\./ && $opt{f}){
		print "       IGNORE-f: $raw$PDS$_\n" if($opt{v}>=2);
		next;
	    }
	    push(@rawlist, $raw.$PDS.$_);
	}
	closedir RDIR;
    }
	# ignore other file types
    if(! -e $raw){
	print "warning: missing file $raw\n";
    }
}

sub checkattr {
    my($kw, $file) = @_;
    my $attr = `git check-attr $kw -- $file`;
    if($attr =~ m/$kw:\s+(.*)/){
		    # This is a bug in git.  What if the value of an attribute is the
		    # string "unset"?  Sigh.
	if(! $opt{F}){
	    if($1 eq "unset" || $1 eq "unspecified"){
		    print "       NOATTR: $attr" if($opt{v}>=2);
		    return 0;
		}
	}
	return 1;
    }
    return 0;
}

# XXX could batch things up - later
sub schedule_work {
    my($file) = @_;
    print "CHECK: '$file'\n" if($opt{v}>=2);
    local($_) = `git status --porcelain --ignored -- $file`;
    my $key = $mode . join('',(m/^(.)(.)/));
    if(length $key == 1){
		# Hack.  An unmodified, tracked file produces no output from
		# git status.  Treat as another version of 'ignored'.
	$key .= '@@';
    }
    $key =~ s/-/ /g;	# for Keni's locally mod'ed git
    if(!exists $codes{$key}){
	die "I'm lost.\nK='$key' F=$file\nST=$_";
    }
    if($codes{$key}==0){
	if($opt{v}>=2){
	    print "       IGNORE: $_" if(length);
	    print "       IGNORE: !! $file\n" if(!length);
	}
	return;
    }
    if($opt{F}){
	    my $ign = `git check-ignore $file`;
	    if($ign !~ m/^\s*$/){
		print "       IGNORE-F: $ign" if($opt{v}>=2);
		return;
	    }
    }

    my $do_nhsubst = &checkattr('NHSUBST', $file);
    my $do_nhdatesub = &checkattr('NH_DATESUB', $file);
    &process_file($file, $do_nhsubst, $do_nhdatesub);
# XXX no longer reachable - parse errors not caught properly?
#	die "Can't parse check-attr return: $attr\n";
}

sub process_file {
    my($file, $do_nhsubst, $do_nhdatesub) = @_;
    print "DOFIL: $file\n" if($opt{v}>=1);
    $count=0;  # if we don't change anything, don't re-write the file

    # For speed we read in the entire file then do the substitutions.
    local($_) = '';
    my $len;
    open INFILE, "<", $file or die "Can't open $file: $!";
    while(1){
	    # On at least some systems we only get 64K.
	my $len = sysread(INFILE, $_, 999999, length($_));
	last if($len == 0);
	die "read failed: $!" unless defined($len);
    }
    close INFILE;

    local $::current_file = $file;	# used under handle*
    if($do_nhsubst){
	    # TODO: This doesn't handle PREFIX-Assert.  To do that,
	    #  we need to capture an entire line, call new sub handlevars()
	    #  and let it iterate across all PREFIX-Foo while
	    #  maintaining $::skip and reverting (like &handledatesub does).
	    #  Also: capture optional arg: $NHDT-Foo(arg)$ $NHDT-Foo(arg): value $

	    # $1 - var and value (including trailing space but not $)
	    # $2 - var
	    # $4 - value or undef
	    # \x24 == $
	s/\$$PREFIX-(([A-Za-z][A-Za-z0-9_]*)(: ([^\x24]+) )?)\$/&handlevar($2,$4,1)/eg;
    }

    if($do_nhdatesub){
	# e.g:
	#.\"DO NOT REMOVE NH_DATESUB .TH MAKEDEFS 6 "DATE(%-d %B %Y)" NETHACK
	#.TH MAKEDEFS 6 "8 February 2022" NETHACK

	# locate the keyword and the rest of the line, capturing the rest of the line (the pattern)
	# use zero width assertions so we can capture them but not replace them (DOES THIS WORK?)
	#and grab the next line (the replaced text)

	# $1 - pattern
	# $2 - the next line (the expanded pattern)
	s/
	    \sNH_DATESUB\s+     # our trigger
	    (.*)[\n\r]+         # save the pattern in $1
	    \K                  # but don't replace it
	    (.*)                # save the oldvalue in $2
	    /
	    &handledatesub($2, $1)      # replace oldvalue
	/emxg;

	#/m  so ^$ match at each line
	#/x  this will be a mess, so attempt to comment it
    }

#print STDERR "COUNT = $count\n";
    return unless($count>0);
    return if($opt{n});

    my $ofile = $file . ".nht";
    my $mode = 0777 & (stat($file))[2];	# save the original mode
    open(TOUT, ">", $ofile) or die "Can't open $ofile";

    my $offset = 0;
    my $sent;
    while($offset < length){
	$sent = syswrite(TOUT, $_, (length($_) - $offset), $offset);
	die "write failed: $!" unless defined($sent);
	last if($sent == (length($_) - $offset));
	$offset += $sent;
    }

    close TOUT or die "Can't close $ofile";
	# Do the right thing for *nix and hope for the best elsewhere:
    chmod($mode, $ofile)==1 or warn "Can't set filemode on $ofile";
    rename $ofile, $file or die "Can't rename $ofile to $file";
}

sub cmdparse {
    my(@in) = @_;

	# What are we doing?
    $opt{cmd} = 'date';	# really nhsub
    if($in[0] eq '--add'){
	$opt{cmd} = 'add';
	shift @in;
    }
    if($in[0] eq '--commit'){
	$opt{cmd} = 'commit';
	shift @in;
    }

# add: -n -v
# commit: --dry-run -v
# nhsub: -n -v
    while($in[0] =~ m/^-/){
	local($_) = $in[0];
	if($_ eq '--'){
	    shift @in;
	    last;
	}
	if(m/^--/){
	    if($opt{cmd} eq 'add' && $_ eq '--dry-run'){
		    exit 0;
	    }
	    if($opt{cmd} eq 'commit' && $_ eq '--dry-run'){
		    exit 0;
	    }
	    if($opt{cmd} eq 'add' && $_ eq '--refresh'){
		    exit 0;
	    }
	    shift @in;
	    next;
	}

	if(m/^-(.*)/){
	    foreach my $single ( split(//,$1) ){
			# don't do -v here from add/commit
		if($single ne 'v'){
			    # don't use -m from add/commit
		    if($opt{cmd} eq 'date' || $single ne 'm'){
			$opt{$single}++;
		    }
		} elsif($opt{cmd} eq 'date'){
		    $opt{$single}++;
		}

		if($opt{cmd} eq 'add' && $single eq 'n'){
		    exit 0;
		}
#need to deal with options that eat a following element (-m, -F etc etc)
#add: nothing?
#commit: -c -C  -F -m 
#   -u<mode> mode is optional
# -S<keyid>  keyid is optional
		if($opt{cmd} eq 'commit'){
		    if($single =~ m/[uS]/){
			last;
		    }
		    if($single =~ m/[cCFm]/){
			    # This will be a mess if the argument is wrong, but can we tell?  No.
			    shift @in;
			    last;
		    }
		}
	    }
	}
	shift @in;
    }

    ($mode) = ($opt{cmd} =~ m/^(.)/);
    $mode = 'f' if($opt{cmd} eq 'date' && ($opt{f}||$opt{F}));
    $mode = 'f' if($opt{cmd} eq 'add' && $opt{f});

    if($opt{cmd} eq 'add' && $#in == -1){
	    # "git nhadd" with no files has nothing to work on
	exit 0;
    }
    if($opt{cmd} eq 'commit' && $#in == -1){
	    # "git nhcommit" with no args handles files already
	    # added, we assume with "git nhadd"
	exit 0;
    }
    if($opt{cmd} eq 'commit' && $opt{a} && $#in == -1){
		# "git commit -a" does multiple things; we only care
		# about modigied files.
#XXX this assumes $RS is set properly for Windows - need to check that
	    my @x = split(/$::RS/,`git ls-files -m`);
	    chomp(@x);
	    push(@in, @x);
    }
    if($opt{cmd} eq 'commit' && $opt{a} && $#in != -1){
		# Let git complain about this for us.
	exit 0;
    }
# "git add" doesn't have a -a option
#	if($opt{cmd} eq 'add' && $opt{a} && $#in != -1){
#		exit 0;
#	}
# I don't know what this was trying to do, but it's wrong.
#	if($opt{cmd} eq 'add' && $opt{a}){
#		my $x = `git rev-parse --show-toplevel`;
#		$x =~ s/[\n\r]+$//;
#		push(@in, $x);
#	}
    return @in;	# this is our file list
}

sub git_config {
    my($section, $var) = @_;
    my $raw = `git config --local --get $section.$var`;
    $raw =~ s/[\r\n]*$//g;
    return $raw if(length $raw);
    die "Missing config var: [$section] $var\n";
}

# (oldvalue, pattern)
sub handledatesub {
    my $oldval = $_[0];	# used if assert fails
    $skip = 0;	# one if assert fails
    my $out = $_[1];	    # the pattern, which we'll edit in place
#print "OLD: '$oldval;\n";
#print "PAT: '$out'\n";
    my $newvalue = $_[1];
    $out =~ s/
	\b	    # don't substitute on a partial word
	(Assert|Date|Branch|Revision|Brev|Project)  # $1 - keyword
	\(					    # (
	([^)]*)					    # $2 - argument
	\)					    # )
	/&onedatesub($1,$2)
	/egx;

    {
	local $::x = $_[0] ne $out;
	if ($::x){
	    $count += $::x;
#	    warn "COUNT++";
	}
    }
#print STDERR "OUT SKIP=$skip count=$count\n";
#print STDERR "OUT old='$oldval'\n    new='$out'\n";
    return ($skip>0 or $count==0) ? $oldval : $out;

}

sub onedatesub {
    my($kw, $arg) = @_;
    my $sname = "PREFIX::$kw";
    die "internal error, '$sname' not defined" unless defined(&$sname);

    no strict;
    my $rv = &$sname(undef, $arg);
    return $rv;
}

#    Assert(P=prefix)
#    Date(format)
#    Branch()
#    Revision()
#    Brev()	Branch:Rev (aBREViated)

sub handlevar {
    my($var, $val, $wrap) = @_;
#print "HV '$var' '$val'\n";
    my $oldval = $val;
    my $subname = "PREFIX::$var";
    if(defined &$subname){
	no strict;
	print "   SUBIN: $var '$val'\n" if($opt{v}>=3);
	$val =~ s/\s+$//;
	$val = &$subname($val, undef);
	print "   SUBOT: $var '$val'\n" if($opt{v}>=3);
	{
	    local $::x =  ($oldval ne $val);
	    if ($::x){
		$count += $::x;
#		warn "COUNT2++ o='$oldval' v='$val'";
	    }
	}
    } else {
	warn "No handler for \$$PREFIX-$var\n";
    }

    if($wrap){
	if(length $val){
		return "\$$PREFIX-$var: $val \$";
	} else {
		return "\$$PREFIX-$var\$";
	}
    } else {
	return $val;
    }
}

sub lsfiles {
    my ($flags, $ps) = @_;
    open RV, "-|", "git ls-files $flags '$ps'" or die "Can't ls-files";
    my @rv = <RV>;
    map { s/[\r\n]+$// } @rv;
    if(!close RV){
	return undef if($! == 0);
	die "close ls-files failed: $!";
    }
    return undef if($#rv == -1);
    return @rv;
}

package PREFIX;
use POSIX qw(strftime);

sub Date {
    my(undef, $val) = @_;
    my $now;

# DONE XXX after several bug fixes, this set of ifs needs some cleanup
my $hash = `git log -1 '--format=format:%H' $::current_file`;
    $now = $^T;
#    if($opt{m} or not defined $hash){

	    # cope with file not yet added to git
    if(not defined $hash){
	if(-f $::current_file){
	    $now = (stat($::current_file))[9];
	} else {
	    die "Can't find file '$::current_file'\n";
	}
    } elsif($opt{m}) {
	    #author keni <keni@his.com> 1429884677 -0400
	chomp($now = `git cat-file -p $hash | awk '/author/{print \$4}'`);
    }
#    } else {
#    }

# DONE XXX simplify this with %s ?
#    my $fmt = length $val ? $val : "%Y/%m/%d %H:%M:%S";
    my $fmt = length $val ? $val : "%s %Y/%m/%d %H:%M:%S";
	# YYYY/MM/DD HH:MM:SS
#    $val = ((length $val==0)?"$now ":"") . strftime($fmt, gmtime($now));
    $val = strftime($fmt, gmtime($now));
    return $val;
}

#sub Header {
#}
#sub Author {
#}

# NB: the standard-ish Revision line isn't enough - you need Branch:Revision -
#     but we split it into 2 so we can use the standard processing code on
#     Revision and just slip Branch in.
# But see new Brev below.
sub Branch {
    my $val;
    $val = `git symbolic-ref -q --short HEAD`;
    $val =~ s/[\n\r]*$//;
    $val =~ s/^\*\s*//;
    $val = "(unknown)" unless($val =~ m/^[[:print:]]+$/);
    return $val;
}

sub Revision {
    my $val;
    my @val = `git log --follow --oneline $::current_file`;
    my $ver = 0+$#val;
    $ver = 0 if($ver < 0);
    $val = "1.$ver";
    return $val;
}

sub Brev {
    my $branch;
    $branch = `git symbolic-ref -q --short HEAD`;
    $branch =~ s/[\n\r]*$//;
    $branch =~ s/^\*\s*//;
    $branch = "(unknown)" unless($branch =~ m/^[[:print:]]+$/);

    my @val = `git log --follow --oneline $::current_file`;
    my $ver = 0+$#val;
    $ver = 0 if($ver < 0);
    $ver = "1.$ver";

    my $val = "$branch:$ver";

    return $val;
}

sub Project {
    my(undef, $arg) = @_;
    my $pn = &::git_config('nethack','projectname');
    if(length $arg == 0){
	;
    } elsif($arg eq 'uc'){
	$pn = uc($pn);
    } else {
	warn "unknown argument '$arg' to Project()\n";
    }
    return $pn;
}


sub Assert {
    my(undef, $val) = @_;

    my($key, $arg) = ($val =~ m/^(.)=(.*)/);
    if(!defined $arg){
	warn "syntax error: Assert($val)\n";
	$::skip = 1;
    }
    my $prefix = $2;

    # P assert arg matches saved prefix
    if('P' eq $key){
	my $repoid = &::git_config('nethack','substprefix');
	if($repoid ne $prefix){
	    $::skip = 1
	}
	return '';
    }

    warn "Unknown Assert type '$key'\n";

    return '';
}

__END__
=for nhgitset nhsub Update substitution variables

=head1 NAME

C<nhsub> - NetHack git command for substitution variables

=head1 SYNOPSIS

C<git nhsub [-v[v[v]] [-n] [-f|-F] [-m] [--] [file...]>

=head1 DESCRIPTION

C<nhsub> rewrites the specified files by doing variable substitution for
variables starting with the prefix specified in the repository's
C<nethack.substprefix> configuration variable.  C<nhsub> is also invoked
internally from the implementation of the C<nhadd> and C<nhcommit>
commands.

The program re-writes those files listed on the command line; if the file
is actually a directory, the program recurses into that directory tree.
Not all files found are re-written; only those with the attribute
NHSUBST (for inline substitutions) or NH_DATESUB (for template
substitution) are considered; finally those with no substitution
variables or no changes due to subtitution variables are not
re-written.  Unless changed by the options, files that have not
changed are not affected.

As a special case, a file not yet added to Git may be the target of
C<nhsub>.

If no files are listed on the command line, the current directory is
checked as if specified as C<.>.
Files listed directly on the command line are always checked.
The C<.git> directory is never processed.

The following command line options are available:

=over

=item C<-v[v[v]]>

Verbose output; may be (usefully) specified up to 3 times.  Not available
when invoked as part of C<nhadd> or C<nhcommit>.

=item C<-n>

Do not write any files.

=item C<-f>

Force, version 1:  
Perform substitution even if the file has not changed,
except no dot files are processed unless listed directly on the command line.
This prevents accidents with editor temporary files while recursing.  Note
that this overloads the C<-f> option of C<git add> and C<git commit>.

=item C<-F>

Force, version 2:
Perform substitution even if the file has not changed,
even if the NHSUBST attribute is not set for the
file, and only if the file is not ignored by git.  Not available
when invoked as part of C<nhadd> or C<nhcommit>.

=item C<-m>

Use metadata (C<git log> and C<git cat-file>) to find the last change date to
substitute.  Often used with C<-f>.  This is useful for cleaning up dates in files that were not
updated when last changed. (Do not use C<git nhadd>/C<git nhcommit> after C<nhsub -m>
or the changes will be overwritten with the current date.)

=back

=head1 SUBSTITUTION VALRIABLES

=over

=item Assert(type=value)

If the assertion fails, no substitutions occur on this line.

If the assertion passes, substitutions occur as usual.

(There is no PREFIX-Assert form at this time, as it would make no sense
for the one Assert type currently implemented.)

=over

=item P

Type C<P> assertions compare the current Git config variable
C<nethack.substprefix>
with the C<value> and succeed if they are identical.

=back

=item Date(format) / PREFIX-Date

This variable usually substitutes the current time but see C<-m> above.

The format of the resulting date varies.  For C<PREFIX-Date> or C<Date()> with
no format, "%s %Y/%m/%d %H:%M:%S" is used.  Otherwise the given strftime
format is used.

=item Branch() / PREFIX-Branch

This variable is replaced with the name of the currently checked out Git
branch.

=item Revision() / PREFIX-Revision

This variable's value emulates a RCS or CVS style revision number.
It consists of "1." followed by the number of commits affecting the
current file.

=item Brev() / PREFIX-Brev

This is a convenience variable that concatenates C<Branch>, a colon, and
C<Revision>.  Short for "aBREViated".

=item Project()

Returns git variable nethack.projectname.  If given the argument C<uc>, returns
an upper-case version of nethack.projectname.

=back

=head1 SUBSTITUTION STYLES

=head2 Prefix Substitution

If a file has the Git attribute C<NHSUBST>, any 
text that looks like a RCS/CVS variable substitution will perform
that substitution.  That is, either of the following forms:

 $Var$
 $Var: Value $

=head2 Template Substitution

If a file has the Git attribute C<NH_DATESUB>, any line that has
the token C<NH_DATESUB> will replace the I<next> line with the
variable expansion of everything after that token.  For example:

 .\"DO NOT REMOVE NH_DATESUB .ds f2 DATE(%B %-d, %Y)
 .ds f2 September 13, 2024

=head1 SEE ALSO

git help gitattributes

perldoc nhgitset.pl
