Source

TimeStampMod / TimestampMod.py

#==============================================================================
# TimestampMod.py - Automatically save and restore the modification times of files
# !!!ALPHA RELEASE!!!
# Copyright 2011 Nathan Durnan <nedmech@gmail.com>
#
# Based on timestamp extension by Friedrich Kastner-Masilko <face@snoopie.at>
# This extension differs from the original in that the original only managed
#	the timestamps of files that had been manually added to the tracking file.
# The default behaviour of this extension instead manages the timestamps of
#	ALL files under version control AUTOMATICALLY.  The purpose of this is to
#	minimize the fears of those who are new to Version Control concepts and
#	make the process of updating between changesets "feel" more like native
#	OS file copy methods (preserving file modification times).
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
#------------------------------------------------------------------------------
# Summary: This is an extension written for Mercurial/TortoiseHG that allows
#	the original modification times of files under version control to be
#	recorded and restored during commit, update, revert, etc. operations.
#
# Development information:
#	Mercurial Version:	1.8.3 (from TortoiseHg package)
#	Python Version:		2.6.6 (from TortoiseHg package)
#	TortoiseHg Version: 2.0.4
#==============================================================================
# begin Extension Help Text:
'''Automatically store and retrieve modification times of files.'''
# end help text

#==============================================================================
# Import Modules
#------------------------------------------------------------------------------
import os	#required for filesystem access methods.
import time	#required for time functions.
from mercurial import localrepo	#required for creating a pseudo-pre-commit hook.
from mercurial import commands	#required for accessing Mercurial commands.
#_ end of imported modules_____________________________________________________

#==============================================================================
# Global Objects
#------------------------------------------------------------------------------
File_TimestampRecords = '.hgtimestamp'
TimeStamp_dict = dict()
List_CommitMatch = []
#_ end of global objects ______________________________________________________



#==============================================================================
# uisetup Callback Configuration
#------------------------------------------------------------------------------
# NOTES: Called when the extension is first loaded and receives a ui object.  
#	This is the FIRST callback that is executed when intializing this extension.
#==============================================================================
def uisetup(ui):
	'''Initialize UI-Level Callback'''
	ui.debug('* Loading TimestampMod uisetup\n')
	ui.setconfig("hooks", "post-status.TimestampMod", Hook_Post_Status)
#_ end of uisetup _____________________________________________________________


#==============================================================================
# extsetup Callback Configuration
#------------------------------------------------------------------------------
# NOTES: Called after all the extension have been initially loaded.
#	This is the SECOND callback that is executed when intializing this extension.
#	It can be used to access other extensions that this one may depend on.
#==============================================================================
def extsetup(ui):
	'''Initialize Extension-Level Callback'''
	ui.debug('* Loading TimestampMod extsetup\n')
	pass
#_ end of extsetup ____________________________________________________________


#==============================================================================
# reposetup Callback Configuration
#------------------------------------------------------------------------------
# NOTES: Called after the main Mercurial repository initialization.
#	This is the LAST callback that is executed when intializing this extension.
#	It can be used to setup any local state the extension might need.
#==============================================================================
def reposetup(ui, repo):
	'''Initialize Repository-Level Callback'''
	ui.debug('* Loading TimestampMod reposetup\n')
	ui.setconfig("hooks", "update.TimestampMod", Hook_Update)
	if not hasattr(localrepo.localrepository, "timestamp_origcommit"):
		'''This is a "dirty" method of wrapping the commit event so
		pre-commit actions are executed.  Normal pre-commit hooks and
		extension.wrapcommand() methods failed to work with the version
		of TortoiseHg used for development (v2.0.4).  Ideally, this
		will be revised to a cleaner method in the future.'''
		localrepo.localrepository.timestamp_origcommit = localrepo.localrepository.commit
		localrepo.localrepository.commit = Wrap_Commit
#_ end of reposetup ___________________________________________________________


#==============================================================================
# Wrap_Commit Function Definition
#------------------------------------------------------------------------------
# Summary: Intercept the commit event to update the timestamp record file, and
#	make sure the record file gets included in the commit.
# NOTES: This is a "dirty" method of wrapping the commit event so pre-commit 
#	actions are executed.  Normal pre-commit hooks and extensions.wrapcommand()
#	 methods failed to work with the version of TortoiseHg used for development 
#	(v2.0.4).  Ideally, this will be revised to a cleaner method in the future.
#==============================================================================
def Wrap_Commit(repo, text="", user=None, date=None, myMatch=None, force=False, editor=False, extra={}):
	repo.ui.debug('Wrap_Commit accessed!\n')
	# Make sure the match object is created.
	if not myMatch:
		myMatch = match.always('')
	else:
		pass
	# End of check for non-existent match object.
	List_CommitMatch = myMatch.files() + [File_TimestampRecords]
	timestamp_mod(repo.ui, repo, **dict({'save': True, 'restore': None}))
	# Make sure record file is part of repository and commit. 
	if not File_TimestampRecords in repo.dirstate:
		repo.ui.debug('Wrap_Commit: ', File_TimestampRecords, ' not in repo.dirstate  Adding...\n')
		commands.add(repo.ui, repo, File_TimestampRecords)
	#end of check for record file in repository.
	#Update match fileset for use under TortoiseHg.
	#  TortoiseHg doesn't seem to catch the updated file
	#  unless it is manually added to the match fileset.
	if File_TimestampRecords in myMatch.files():
		# Don't need to do anything if record file already in commit.
		repo.ui.debug('Wrap_Commit: ', File_TimestampRecords, ' already in match\n')
		pass
	else:
		# Add the record file to the match object.
		myMatch.files().append(File_TimestampRecords)
	# end of check for record file.
	repo.ui.debug('Match Files: ', str(myMatch.files()), '\n')
	return repo.timestamp_origcommit(text, user, date, myMatch, force, editor, extra)
#_ end of Wrap_Commit _________________________________________________________


#==============================================================================
# Hook Function Definitions
#------------------------------------------------------------------------------
# Summary: These functions are intended to be triggered by the hooks defined
#	either by Mercurial or in the configuration files.
# NOTES: The pre-commit hook is the only one that does not function properly
#	under the TortoiseHg (v2.0.4) GUI.  All the hooks work from command-line.
#==============================================================================
def Hook_Pre_Commit(repo, **kwargs):
	timestamp_mod(repo.ui, repo, **dict({'save': True, 'restore': None}))
	kwargs['pats'].append(File_TimestampRecords)

def Hook_Post_Status(repo, **kwargs):
	timestamp_mod(repo.ui, repo, **dict({'save': None, 'restore': None}))

def Hook_Update(repo, **kwargs):
	timestamp_mod(repo.ui, repo, **dict({'save': None, 'restore': True}))
#_ end of Hook Functions ______________________________________________________


#==============================================================================
# timestamp_mod 
#------------------------------------------------------------------------------
# Summary: save or restore file modification times.
#
#==============================================================================
def timestamp_mod(ui, repo, **opts):
	'''Save or restore file modification times.'''
	ui.note("Executing timestamp_mod function\n")
	
	# Retrieve Repository file list contents.
	myChangedList = list()
	myDroppedList = list()
	myChangedList, myDroppedList = _get_RepoFileList(repo)
	
	# Retrieve existing timestamps from the record file.
	_read_TimestampRecords(repo)
	
	# Check for command optional argument
	if opts['save']:
		_save_Timestamps(repo, myChangedList)
	elif opts['restore']:
		_restore_Timestamps(repo)
	else:
		_display_Timestamps(repo)
	# end of check options
#_ end of timestamp_mod _______________________________________________________



#==============================================================================
# Command Table Definition
#------------------------------------------------------------------------------
# (NOTE: Keep this after command definitions.  cmdtable contents 
#		 must be defined after the commands/functions referenced!)
#==============================================================================
cmdtable = {
	"timestamp_mod":
		(timestamp_mod, 
			[('s', 'save', None, ('save modification times')),
			('r', 'restore', None, ('restore modification times'))
			], 
			('hg timestamp_mod [-s | -r]')
		) #end of timestamp_mod
}
#_ end of cmdtable ____________________________________________________________



#==============================================================================
# _get_fileModTime() Function Definition
#------------------------------------------------------------------------------
# Summary: Return a UTC timestamp value for the specified file's modified time.
#==============================================================================
def _get_fileModTime(repo, myFileName):
	'''Retrieve the Modification Timestamp for the specified file.'''
	repo.ui.debug('get_mtime: ')
	myFilePath = repo.wjoin(myFileName)
	try:
		myModTime = float(os.stat(myFilePath).st_mtime)
		repo.ui.note(time.strftime("%Y.%m.%d %H:%M:%S", time.localtime(myModTime)), " \t", myFileName, "\n")
		return myModTime
	except:
		repo.ui.warn('*** File stat failed for ', myFileName, '!\n')
		return -1
	# end of file stat access.
#_ end of _get_fileModTime ____________________________________________________



#==============================================================================
# _set_fileModTime() Function Definition
#------------------------------------------------------------------------------
# Summary: Set the UTC timestamp value for the specified file's modified time.
#==============================================================================
def _set_fileModTime(repo, myFileName, myModTime):
	'''Assign the Modification Timestamp for the specified file.'''
	repo.ui.debug('set_mtime: ')
	myFilePath = repo.wjoin(myFileName)
	try:
		myFileStat = os.stat(myFilePath)
		os.utime(myFilePath, (myFileStat.st_atime, type(myFileStat.st_mtime)(myModTime)))
		repo.ui.note(time.strftime("%Y.%m.%d %H:%M:%S", time.localtime(myModTime)), " \t", myFileName, "\n")
	except:
		repo.ui.warn('*** File stat failed for ', myFileName, '! ***\n')
	# end of file stat access.
#_ end of _set_fileModTime ____________________________________________________



#==============================================================================
# _get_RepoFileList Function Definition
#------------------------------------------------------------------------------
# Summary: Build lists of the files in the Working Directory from the Repository
#	Status entries.  Add active files (clean/added/modified) to the global 
#	dictionary collection, andreturn lists containing changed files (added/
#	modified) and dropped files(removed/deleted).  
# NOTE: This function will CLEAR the contents of the global dictionary collection
#	and rebuild it from scratch.
#==============================================================================
def _get_RepoFileList(repo):
	'''Build lists of files from the repository status contents.'''
	repo.ui.debug('______\nGenerating file list from repo...\n------\n')
	# Establish category lists from repository status.
	modified, added, removed, deleted, unknown, ignored, clean = repo.status(ignored=True, clean=True, unknown=True)
	myChanged = List_CommitMatch + modified + added
	myDropped = removed + deleted
	myFiles = myChanged + clean
	# Rebuild global dictionary collection
	TimeStamp_dict.clear() # Be sure to start with a clean collection.
	for myFile in myFiles:
		myFileName = str(myFile).strip()
		TimeStamp_dict[myFileName] = -1	# initialize dictionary entry
		repo.ui.debug('Tracking:  ', myFileName, '\n')
	# end of loop through repo files.
	myReturnList = myChanged, myDropped
	return myReturnList
#_ end of _get_RepoFileList ___________________________________________________



#==============================================================================
# _read_TimestampRecords Function Definition
#------------------------------------------------------------------------------
# Summary: Read in the data from the Timestamp Record File and assign the 
#	timestamps to their corresponding entries in the global file dictionary
#	collection object.
#==============================================================================
def _read_TimestampRecords(repo):
	'''Read data from Timestamp Record File.'''
	myTimeStampRecordsFile = ''
	try:
		myTimeStampRecordsFile = file(repo.wjoin(File_TimestampRecords), 'r')
		repo.ui.debug('______\nRetrieveing timestamps from record file:\n------\n')
		for myLine in myTimeStampRecordsFile.readlines():
			# Read the data from the line. (CSV format: [FileName],[ModificationTime])
			try:
				myFileName, myModTime = myLine.strip().split(',')
				if myFileName in TimeStamp_dict:
					TimeStamp_dict[myFileName] = float(myModTime)
					repo.ui.debug('UTC: ', myModTime, '\t: ', myFileName, '\n')
				# end of check file exists in dictionary.
			except:
				repo.ui.debug('Invalid data on line: ', myLine)
			# end of read data from line.
		# end of readlines from record file.
		myTimeStampRecordsFile.close()
	except:
		repo.ui.warn('*** Error accessing ', File_TimestampRecords, ' file! ***\n')
	# end of accessing record file.
#_ end of _read_TimestampRecords ______________________________________________



#==============================================================================
# _save_Timestamps Function Definition
#------------------------------------------------------------------------------
# Summary: Save File Modified Timestamp data for files in the global dictionary
#	collection to a record file in the repository root directory.  New values
#	for timestamps will be retrieved for files that have changed or are missing
#	timestamp records in the first place.
#==============================================================================
def _save_Timestamps(repo, myChangedList):
	'''Save File Modification Timestamps to record file.'''
	repo.ui.note('______\nSaving timestamps...\n------\n')
	myTimeStampRecordsFile = file(repo.wjoin(File_TimestampRecords), 'w')
	# Remove the record file from the list to loop through.
	#	It will be updated separately at the end.
	if File_TimestampRecords in TimeStamp_dict:
		del TimeStamp_dict[File_TimestampRecords]
	# end of check for record file.
	for s_fileName, f_fileModTime in TimeStamp_dict.items():
		if (s_fileName in myChangedList) or (f_fileModTime<=0):
			f_fileModTime = TimeStamp_dict[s_fileName] = _get_fileModTime(repo, s_fileName)
		# end of update timestamps for changed items or missing timestamps.
		myTimeStampRecordsFile.write("%s,%s\n" % (s_fileName, f_fileModTime))
	# end of loop through dictionary items.
	# Update the record file timestamp last with the current time.
	f_fileModTime = TimeStamp_dict[File_TimestampRecords] = time.time()
	myTimeStampRecordsFile.write("%s,%s\n" % (File_TimestampRecords, f_fileModTime))
	# Make sure to close the file!
	myTimeStampRecordsFile.close()
#_ end of _save_Timestamps ____________________________________________________



#==============================================================================
# _restore_Timestamps Function Definition
#------------------------------------------------------------------------------
# Summary: Restore the File Modification Timestamp property for files in the
#	global dictionary collection.  
# NOTE: This presumes that the dictionary has been initialized and the original
#	timestamp data has already been read into the dictionary.
#==============================================================================
def _restore_Timestamps(repo):
	'''Restore File Modification Timestamps from record file.'''
	repo.ui.note('______\nRestoring timestamps...\n------\n')
	for s_fileName, f_fileModTime in TimeStamp_dict.items():
		# Check for valid timestamp.
		if (f_fileModTime>0):
			# Valid timestamp detected!
			# Restore file timestamp.
			_set_fileModTime(repo, s_fileName, f_fileModTime)
		else:
			# No valid timestamp recorded, skip this file.
			repo.ui.note(s_fileName, ' - skipped - no timestamp recorded\n')
		# end of check for valid timestamp.
	# end of loop through dictionary items.
#_ end of _restore_Timestamps _________________________________________________



#==============================================================================
# _display_Timestamps Function Definition
#------------------------------------------------------------------------------
# Summary: Default action.  Display the File Modification Timestamp property for
#	files in the global dictionary collection.  
# NOTE: This presumes that the dictionary has been initialized and the original
#	timestamp data has already been read into the dictionary.
#==============================================================================
def _display_Timestamps(repo):
	'''Display File Timestamps currently recorded.'''
	repo.ui.note('______\nDisplaying timestamps...\n------\n')
	for s_fileName, f_fileModTime in TimeStamp_dict.items():
		# Check for valid timestamp
		if (f_fileModTime>0):
			# Valid timestamp detected!
			# Display timestamp using local time adjustment.
			repo.ui.status(time.strftime("%Y.%m.%d %H:%M:%S", time.localtime(f_fileModTime)), " \t", s_fileName, "\n")
		else:
			# No valid timestamp recorded, skip this file.
			repo.ui.note(s_fileName, ' - skipped - no timestamp recorded\n')
		# end of check for valid timestamp.
	# end of loop through dictionary items.
#_ end of _display_Timestamps _________________________________________________