tshm
1/31/2014 - 8:23 AM

clearcase_git.thor

# Thorfile tasks/functions for git/clearcase integration
require 'ruby-debug'
require 'time'

class Cc < Thor
	include Thor::Actions
	@@git_root = "./" + `git rev-parse --show-cdup`.strip
	@@cleartool =
		(str = `git config clearcase.cleartool`.strip).empty? ? "cleartool" : str
	@@ccbr =
		(str = `git config clearcase.trackingbranch`.strip).empty? ? "master" : str
	@@grace_sec = 60 * 1

	desc 'test', 'test task'
	method_options %w( logfile -l ) => :string
	def test
		if options[:logfile] && File.exist?(options[:logfile])
			say options[:logfile], :green
		else
			say 'no logfile specified', :red
		end
		if run "ls tmp"
			say "succ", :green
		else
			say "failed", :red
		end
	end

	no_tasks do
		def run_git(command, flag=nil)
			config = ( :capture == flag ? {:capture => true} : {})
			run("git #{command}", config)
		end

		def run_ct(command, flag=nil)
			config = ( :capture == flag ? {:capture => true} : {})
			run("#{@@cleartool} #{command}", config)
		end

		def isclean()
			inside(@@git_root) do
				status = run_git("status --porcelain", :capture)
				return true if 0 == status.length
				say status
				say "-------- dirty workspace ------", :red
				return false
			end
		end

		def stage_ccobjects(objects)
			objects.each do |o|
				fn = o.sub(/@@.*$/,'')
				run "rm -f #{fn}"
				run_ct "get -to #{fn} '#{o}'"
				run_git "add #{fn}"
			end
		end

		def convert_changes(loghash, dry_run = false)
			num_commits = 0
			loghash.each do |k,v|
				commit_time = v[0][:date]
				say "===  commit: [#{num_commits+=1}/#{loghash.length}] ====", :green
				say "user: #{k[:user]}"
				say "action: #{k[:action]}"
				say "log: #{k[:comment]}"
				say "date: #{commit_time}"
				say "objects:\n" + v.map{|i| "  #{i[:object]}"}.join("\n")
				open(".cc_commit_message", "w") do |f|
					f.puts k[:comment] + "\n\n"
					f.puts v.map{|i| "  #{i[:object]}"}.join("\n")
				end
				objects = v.map{|i| i[:object]}.uniq
				if dry_run
					run_git "status"
					say "======== dry_run: not commiting ==========", :red
					next
				end
				stage_ccobjects(objects)
				ENV["GIT_COMMITTER_NAME"]  = k[:user]
				ENV["GIT_AUTHOR_NAME"]     = k[:user]
				ENV["GIT_COMMITTER_EMAIL"] = " "
				ENV["GIT_AUTHOR_EMAIL"]    = " "
				ENV["GIT_COMMITTER_DATE"]  = v[0][:date]
				ENV["GIT_AUTHOR_DATE"]     = v[0][:date]
				run_git "commit -F .cc_commit_message"
			end
		end

		def analize_cchistory(cchistory, elems = {})
			p cchistory
			logs = cchistory.split(/--/)
			return unless logs
			loghash = Hash.new {|h,k| h[k] = []}
			logs.each do |item|
				h = item.split(/###\s+###/)
				next unless h[3]
				hh = h[3].split(/@@/)
				key = {
					:user => h[1],
					:action => h[2],
					:comment => (h[4] || "").strip
				}
				val = {
					:object => h[3],
					:file => hh[0],
					:date => h[0]
				}
				if key[:comment].empty?
					key[:comment] = "<<NO COMMENT @ #{val[:date].sub(/:\d\d-/,':00-')}>>"
				end
				if !elems[val[:file]] or elems[val[:file]].empty? or val[:object] =~ /#{elems[val[:file]]}\\\d+$/
					loghash[key] << val
				end
			end
			return Hash[ loghash.map {|k,v| [k, v.sort_by {|x| x[:date]}.reverse] } ]
		end

		def cc_update_view(time = nil)
			cs = run_ct("catcs", :capture)
			exit if cs.empty?
			if time
				if cs =~ /^time/
					cs.sub!(/time .*(\s+)/) { "time " + time + $1 }
				else
					cs.sub!(/element \* CHECKEDOUT(\s+)/) {|x| x + "time " + time + $1}
				end
			else
				cs.sub!(/^time .*\s*/, '')
			end
			open(".configspec", "w") {|f| f.print cs}
			max_count = 0
			say "=========== updating clearcase view...  ==========="
			if File.exists?(".cc_update_lines")
				open(".cc_update_lines") {|f| max_count = f.gets.to_i}
			end
			open("| #{@@cleartool} setcs -force -overwrite .configspec", "r") do |io|
				io.each do |line|
					$stdout.print( max_count==0 ?
						"\r#{io.lineno} lines" :
						"\r#{100 * io.lineno / max_count} %" )
					$stdout.flush
				end
				open(".cc_update_lines", "w") {|file| file.print io.lineno}
			end
			say ""
		end

		def git_current_branch()
			org_branch = run_git("branch", :capture)[/\*.*/][2..-1]
			if "(no branch)" == org_branch
				say "=========== aborting: not on any branch. =========", :red
				exit
			end
			return org_branch
		end

		def git_checkout_branch(org_branch)
			unless @@ccbr == org_branch
				say "----------- checking out original branch -----------"
				run_git "checkout -f #{org_branch}"
			end
		end

		def get_cc_history_bkup(files)
			# Analyze git log for last pull time for changed files (pick the oldest commit)
			time_format = '%d-%B-%Y.%H:%M:%S'
			gitlog = run_git("log --pretty=format:'%ae####%cD'", :capture)
			cclog = gitlog.grep(/^####/)  # gitlog of the cc following branch
			str_git_last_time =
				if (cclog && cclog[0])
					cclog[0][4..-1].strip
				else
					# there is no cclog, then time of the git repo initialization
					gitlog.split(/####/)[-1].strip
				end
			last_time = (Time.rfc2822(str_git_last_time) - @@grace_sec).strftime(time_format)
			# get clearcase history data for target time & files
			logfmt = "'%d###\n###%u###\n###%e###\n###%n###\n###%c--'"
			lshistory_params = "-fmt #{logfmt} -since #{last_time} #{files.join(' ')}"
			cchistory = run_ct("lshistory #{lshistory_params}", :capture)
			return analize_cchistory(cchistory)
		end

		def get_cc_history(files)
			# Analyze git log for last pull time for changed files (pick the oldest commit)
			time_format = '%d-%B-%Y.%H:%M:%S'
			gitlog = run_git("log --pretty=format:'%ae####%cD'", :capture)
			cclog = gitlog.grep(/^####/)  # gitlog of the cc following branch
			str_git_last_time =
				if (cclog && cclog[0])
					cclog[0][4..-1].strip
				else
					# there is no cclog, then time of the git repo initialization
					gitlog.split(/####/)[-1].strip
				end
			pp last_time = (Time.rfc2822(str_git_last_time) - @@grace_sec).strftime(time_format)
			# get clearcase visible branch for the target files.
			branches = run_ct("describe -fmt '%n\n' #{files.join(' ')}", :capture).gsub(/.+\\(.+)\\(.+)/, '\1').split(/\s+/)
			pp elems = Hash[files.zip(branches)]
			# get clearcase history data for target time & files
			logfmt = "'%d###\n###%u###\n###%e###\n###%n###\n###%c--'"
			lshistory_params = "-fmt #{logfmt} -since #{last_time} #{files.join(' ')}"
			cchistory = run_ct("lshistory #{lshistory_params}", :capture)
			return analize_cchistory(cchistory, elems)
		end
	end

	desc 'test', 'cmd test'
	def test
		files = ["Workstation/SynapseVer.h", "HTML/SynapseScripts/ExecuteNextStudy.js"]
		get_cc_history(files)
	end

	desc "init [trackingbranch]", 'initialize cc tracking branch'
	def init(trackingbranch = "master")
		run_git "init"
		run_git "config clearcase.trackingbranch #{trackingbranch}"
		run "echo 'Thorfile' >| .git/info/exclude"
		ENV["GIT_AUTHOR_EMAIL"] = " "
		run_git "add ."
		run_git "commit -m 'init'"
		run_git "tag init"
	end

	desc "update [time]", 'force CC update into workspace without checkins'
	def update(time = nil)
		exit unless isclean()
		cc_update_view(time)
		inside(@@git_root) do
			#run "chmod -R +w ../SourceCode"
			run_git "status"
		end
	end

	desc 'pull', 'pull changes from upstream CC into repo'
	method_options %w( dry-run -n ) => :boolean
	def pull
		exit unless isclean()
		org_branch = git_current_branch()
		inside(@@git_root) do
			begin
				run_git "checkout #{@@ccbr}"
				cc_update_view()
				say "========= track changes ==========="
				say status = run_git("status --porcelain", :capture)
				0 == status.length and raise "==== No upstream change detected. ===="
				loghash = get_cc_history(status.split(/\n/).map{|line| line[3..-1]})
				open(".cclog", "w") {|fp| fp.print loghash.inspect}
				if loghash
					loghash = loghash.select{|k,v| k[:action]=~/create/}.sort_by{|i| i[1][0][:date]}
				end
				convert_changes(loghash, options["dry-run"]) unless loghash.empty?
				# re-update and checkin the changes.
				cc_update_view()
				if !run_git("status --porcelain", :capture).empty?
					say "==== comming the detected upstream changes ====", :green
					if options["dry-run"]
						say "===== dry run: not merging or commiting =====", :green
					else
						run_git "add --all"
						run_git "commit -m '<UPSTREAM CHANGE COULD NOT BE TRACKED>'"
					end
				end
			rescue => evar
				say evar, :red
			ensure
				#run "chmod -R +w ../SourceCode"
				git_checkout_branch(org_branch)
			end
		end
	end

	desc 'merge [target_branch]', "merge changesets from specified target (default current) branch to #{@@ccbr} and commit them to CC"
	method_options %w( dry-run -n ) => :boolean
	method_options %w( file -F ) => :string
	def merge(tgt_branch = nil)
		exit unless isclean()
		org_branch = git_current_branch()
		tgt_branch ||= org_branch
		logfile = options[:file]
		begin
			say "----------- checking out #{@@ccbr} -----------"
			run_git "checkout #{@@ccbr}"
			stat = run_git("diff --name-status ...#{tgt_branch}", :capture)
			stat.empty? and raise "---- no difference ---"
			mod_files = stat.split(/\n/).map{|s| s.gsub!(/^[^A]\t/,'')}.compact
			add_files = stat.split(/\n/).map{|s| s.gsub!(/^A\t/,'')}.compact
			mod_filestr = (mod_files + add_files.map{|fn| File.dirname(fn)}).uniq.join(" ")
			add_filestr = add_files.join(" ")
			run_ct "update -overwrite #{mod_filestr}"
			isclean() or raise "---- detected upstream change.  aborting... ------"
			unless logfile && File.exist?(logfile)
				run_git "log --format=format:'- %s%n%b%n' #{@@ccbr}..#{tgt_branch} > .cc.log"
				run "cp .cc.log .cc.log.bkup"
				run "$EDITOR .cc.log"
				run "diff .cc.log .cc.log.bkup" and raise "commit log has to be updated."
				logfile = ".cc.log"
			end
			unless run_ct "checkout -nquery -reserved -cfile #{logfile} #{mod_filestr}"
				run_ct "uncheckout -rm #{mod_filestr}"
				raise "----------- clearcase checkout failed -------------"
			end
			if options["dry-run"]
				say "===== dry run: not merging or commiting =====", :green
			else
				run_git "merge --no-ff --no-commit --stat #{tgt_branch}"
				run_ct "mkelem -cfile #{logfile} -ci #{add_filestr}" if !add_files.empty?
				say "===== do clearcase & git commiting =====", :green
				run_ct "checkin -identical -nc #{mod_filestr}"
				run_git "commit -F #{logfile}"
			end
		rescue => evar
			say "aborting the merge: #{evar}", :red
		ensure
			#say "========= change permission ==========="
			#run "chmod -R +w ../SourceCode"
			run_git "reset --hard"
			git_checkout_branch(org_branch)
		end
	end
end

# vim: ft=ruby