git timestamps

Git timestamps

Sometimes, people want to have timestamps of files (mtime) preserved, especially when documents or binary files are subject to versioning. People may also like it when a source file collection that is not subject to make processes have original time stamps preserved through checkout of branches or versions. This is an attempt to add this feature.

Two hooks do the job, a pre-commit hook and a post-checkout hook. The mechanism has been tested to work when "standard" things are going on (it is tested with checkouts in a linear history). Since git is also a real hacker tool, use cases that could infer the timestamp processing should be considered when such special uses are going on. Example: git reset --hard. In some circumstances, git checkout -- . have the same effect (but restores timestamps).

Concept

Upon commit, a file .git_mtimes will be created. It contains the names of all files that are subject to commit (new, modified) together with their timestamps. This special file must be versioned.

Upon checkout, .git_mtimes is scanned and each file that has a timestamp close to the current time is treated as updated, so this is safe with branch checkouts as well as with file checkouts. Additionally, a filter file .gitignore_mtime may be used to prevent files, or file classes from time stamp restoration. Typical content could be:

 *.c
 *.h

For any cases where original timestamps of all changes in a commit must be retrieved temporarily, this file can be renamed for this case, or other options could be considered for this. Theoretically, all timestamps of a particular revision could be restored, when the post-checkout hook would dig in the history for last commits of particular file.

Code

Both files are stored in .git/hooks/

This is the 2nd iteration. Time stamps are stored in human readable format now, and stored in decreasing time order.

RLE (2011-01-16)

3rd generation - fix issues with file names containing spaces or special characters (to Tcl).

pre-commit

#!/usr/bin/tclsh
# pre-commit has no parameters
# Save all mtimes and filenames that are subject to commit.
# TODO:  All timestamps of a given active file set per commit must be known
#        for allowing branch checkouts and also simple backward checkouts.
#        Two approaches:
#        1. create a patch file that is applied to .git_mtimes just before
#           commit executed (or modify file directly) - can reach big size.
#           (would then tend to skip those in .gitignore_mtime)
#        2. have each checkout take care of this: if no timestamp found
#           (because not modified), search history for commit where file 
#           is changed, and query its version of .git_timestamp.
proc zsplit { str } {
  split [ string trimright $str \x00 ] \x00
}

proc save_mtimes {} {
    set fid [open .git_mtimes w+]  ;# TODO: on git --amend should append
    set outlist [ list ]
    if {[catch {zsplit [ exec git diff --cached --name-status -z ]} inlist]} {
        # when initial commit
        set temp [ zsplit [ exec git status -uno --porcelain -z ] ]
        set inlist [ list ]
        foreach item $inlist {
          lappend inlist [ list M [ string range $item 3 end ] ]
        }
    }
    foreach {stat fname} $inlist {
        if {![file exists $fname]} continue
        lappend outlist [ list [clock format [file mtime $fname] -format "%Y-%m-%d %T"] $fname ]
    }
    puts $fid [join [lsort -decreasing $outlist] \n ]
    close $fid
    exec git add .git_mtimes
}

save_mtimes
exit 0

post-checkout

#!/usr/bin/tclsh
# post-checkout arguments:
#     ref prev. HEAD; ref new HEAD; branch(1)/file(0) checkout

# Restore all mtimes of files that are touched no longer than 5 s ago
# and restore their file mtime when filename is not in .gitignore_mtime.
proc update_mtimes {type} {
    # type not yet used
    set filterlist ""
    if {[file exists .gitignore_mtime]} {
        set fid [open .gitignore_mtime r]
        set filterlist [read $fid]
        close $fid
    }
    set fid [open .git_mtimes r]
    set reftime [clock seconds]
    set count 0
    while {![eof $fid]} {
        lassign [gets $fid] datetime fname
        if {$fname == ""} break
        if {[file mtime $fname] > $reftime - 10} {
            # if file apparently updated, then check ignorelist
            set match 0
            foreach filter $filterlist {
                if {[string match $filter $fname]} {
                    set match 1
                }
            }
            if {!$match} {
                file mtime $fname [clock scan "$datetime"]
                incr count
            }
        }
    }
    close $fid
    if {$count} {
        puts "$count file(s) have restored mtime"
    }
}

if {![file exists .git_mtimes]} {
    exit 0
}
update_mtimes [lindex argv 2]
exit 0

Upon first configuration an empty file .git_mtimes must be created and git add must be applied. Otherwise, it is not in the commit. Note: these hooks are provided "as is" - no liability for any problems arising from its usage. Improvements welcome... (RJM)