For fun, I used the open source gource app to generate this video. http://gource.io/

 

To create it I had to pull changelist data from our perforce server and conform it to a log format that gource would use. To accomplish this, I made this quick little python script, intended to be run from a console.

python script.py "depot_path" "output.gource"
gource output.gource -o gource.ppm

With the “script.py” being this script:

import P4
import os
import sys
from optparse import OptionParser

P4_ACTION_TO_GOURCE = {
	"add"				: "A" ,
	"branch"			: "A" ,
	"edit"			: "M" ,
	"move/add"		: "M" ,
	"integrate"		: "M" ,
	"delete"			: "D" ,
	"move/delete"	: "D",
	"purge"			: "D"
}

class P4_Connect( object ):
	"""
	Context manager that connects a p4 instance on entry if it is not connected and disconnects on exit.

	Also, p4 exception level is raised to Errors.

	**Arguments:**
		:``None``: ``

	**Keyword_Arguments:**
		:``p4_instance``: `P4.P4` Optional P4 instance rather than creating one on the fly.

	**Returns:**
		:``None``: ``

	**Examples:** ::
		>>> with P4_Connect() as p4:
				p4.run_sync()

	**Author:**
		* kelly.snapka, kelly.snapka@volition-inc.com, 6/25/2016 9:05 PM
	"""
	def __init__( self, p4_instance=None ):
		self.p4_instance						= p4_instance or P4.P4()
		self.exception_level					= self.p4_instance.exception_level
		self.p4_instance.exception_level = P4.P4.RAISE_ERROR


	def __enter__( self ):
		self.isConnected = self.p4_instance.connected()
		if not self.isConnected:
			self.p4_instance.connect()

		return self.p4_instance


	def __exit__( self, type, value, tb ):
		self.p4_instance.exception_level = self.exception_level
		if not self.isConnected:
			# Only disconnect if a connection was made in the __enter__ method
			self.p4_instance.disconnect()


def printProgress (iteration, total, prefix = '', suffix = '', decimals = 1, barLength = 100):
	"""
	Call in a loop to create terminal progress bar
	@params:
		iteration   - Required  : current iteration (Int)
		total       - Required  : total iterations (Int)
		prefix      - Optional  : prefix string (Str)
		suffix      - Optional  : suffix string (Str)
		decimals    - Optional  : positive number of decimals in percent complete (Int)
		barLength   - Optional  : character length of bar (Int)
	"""
	formatStr       = "{0:." + str(decimals) + "f}"
	percents        = formatStr.format(100 * (iteration / float(total)))
	filledLength    = int(round(barLength * iteration / float(total)))
	bar             = '*' * filledLength + '-' * (barLength - filledLength)
	sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)),
	if iteration == total:
		sys.stdout.write('\n')
	sys.stdout.flush()
	

def get_options():
	usage		= "Usage: %prog [options] DEPOT_PATH OUTPUT_FILE"
	parser 	= OptionParser( usage )
	return parser.parse_args()

def build_log( depot_path, *args ):
	log = ''
	with P4_Connect() as p4:
		changes = p4.run_changes( depot_path, *args )
		changelist_numbers = [ d['change'] for d in changes ]
		changelist_numbers.reverse()

		_depot_path = depot_path.split("...")[0]
		num_changelists = len( changelist_numbers )
		for n in range( num_changelists ):
			changelist_number = changelist_numbers[ n ]
			printProgress( n+1, num_changelists, prefix = 'Progress:', suffix = 'Complete', barLength = 50)
			descriptions = p4.run_describe( '-s', changelist_number )

			for d in descriptions:
				timestamp	= d.get( 'time' )
				author		= d.get( 'user' )

				num_files = len( d.get( 'depotFile' ) )
				for i in range( num_files ):
					action		= P4_ACTION_TO_GOURCE[ d.get( 'action' )[i] ]
					depot_file	= d.get( 'depotFile' )[i]

					if _depot_path in depot_file:
						log += '{0}|{1}|{2}|{3}\n'.format( timestamp, author, action, depot_file )

	return log


def write_log_to_disk( p4_log, gource_file ):
	with open( gource_file, 'w' ) as file:
		file.write( p4_log )


if __name__ == "__main__":
	options, args	= get_options()
	depot_path		= args[0]
	gource_file		= args[1]

	p4_log = build_log( depot_path )
	write_log_to_disk( p4_log, gource_file )