{"thread":{"id":"37994","subject":"[PATCH/RFC] Add contrib Clearcase Base import utility","startedAt":"2014-11-18T04:15:14Z","lastAt":"2014-11-18T04:15:14Z","messageCount":1,"participants":["Jason Woodward"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"252074","messageId":"1416284114-9111-1-git-send-email-woodwardj@jaos.org","threadId":"37994","inReplyTo":null,"subject":"[PATCH/RFC] Add contrib Clearcase Base import utility","fromName":"Jason Woodward","fromEmail":"woodwardj@jaos.org","sentAt":"2014-11-18T04:15:14Z","receivedAt":"2014-11-18T04:15:14Z","isPatch":true,"sender":{"key":"woodwardj@jaos.org","avatar":null},"body":"This is a simple perl script that dumps the history of a Base Clearcase\nVOB and then walks this history retrieving the file contents, version, and\nbranch information.  Equivalent git commits are created from the Clearcase\nhistory via git-fast-import(1).\n\nThis does not support Clearcase UCM.\n\nSigned-off-by: Jason Woodward <woodwardj@jaos.org>\n---\n contrib/cc-base-import/cc-base-import     | 579 ++++++++++++++++++++++++++++++\n contrib/cc-base-import/cc-base-import.txt | 111 ++++++\n 2 files changed, 690 insertions(+)\n create mode 100755 contrib/cc-base-import/cc-base-import\n create mode 100644 contrib/cc-base-import/cc-base-import.txt\n\ndiff --git a/contrib/cc-base-import/cc-base-import b/contrib/cc-base-import/cc-base-import\nnew file mode 100755\nindex 0000000..4a89703\n--- /dev/null\n+++ b/contrib/cc-base-import/cc-base-import\n@@ -0,0 +1,579 @@\n+#!/usr/bin/perl\n+use strict;\n+use warnings;\n+use POSIX qw/mktime/;\n+use File::Spec qw/splitdir catdir join splitpath curdir rel2abs/;\n+use IPC::Open3 qw/open3/;\n+use Getopt::Long qw/GetOptions/;\n+\n+=head1 NAME\n+\n+cc-base-import - Import a ClearCase VOB into git\n+\n+=head1 SYNOPSIS\n+\n+cc-base-import [command] [option(s)]\n+\n+commands: init --vob <path> | fetch | commit\n+\n+options: [--verbose | --savetemps]\n+\n+example:\n+\n+ $ mkdir /tmp/project\n+ $ cd /tmp/project\n+ $ git cc-base-import init --vob /view/view_main/vobs/vob1 --verbose\n+Initialized empty Git repository in /tmp/project/.git/\n+ $ git cc-base-import fetch --verbose\n+Fetching clearcase events\n+ $ git cc-base-import commit\n+... [generates git commits from clearcase events]\n+ $ git checkout master\n+ $ ls\n+...\n+\n+=head1 DESCRIPTION\n+\n+This utility walks the history of a ClearCase VOB creating equivalent commits\n+in git.  Branch names, commit messages, and author information is preserved.\n+\n+The only requirement is the B<cleartool> utility must be present in the current\n+PATH.\n+\n+B<NOTE> this does not work with ClearCase UCM, only base ClearCase.\n+\n+\n+=head2 Error Handling\n+\n+Certain data loss messages are currently ignored, such as the following:\n+\n+  cleartool: Error: Operation \"file copy\" failed: No such device or address.\n+\n+This implies that for some reason a version of the file in question is not available.\n+This is ignored in hopes that a later version is available from Clearcase.\n+\n+Other errors are not, such as:\n+\n+  cleartool: Error: Operation \"file copy\" failed: Input/output error.\n+\n+This error implies some underlying issue such as MVFS communication errors.\n+\n+  cleartool: Error: Operation \"get cleartext\" failed: not a ClearCase object.\n+\n+This error indicates a file was deleted during a long running import.\n+\n+=head2 Deficiencies\n+\n+There is no attempt to handle tracking branch renames in clearcase.\n+\n+File deletions are also not handled.  Some info can be gleamed from the directory\n+history but that information is not enforced and can be overridden.  This is\n+also true for file moves.\n+\n+A suggested strategy is to run the conversion and enforce no file moves,\n+deletions, or branch renames during a set window.\n+\n+Merge arrows and related clearcase meta data are not preserved in the conversion,\n+only the file contents.\n+\n+Labels are not yet accounted for.  Since labels do not represent an atomic operation\n+in clearcase, there is not a general way to represent a git tag as a clearcase label.\n+\n+=head1 COMMANDS\n+\n+=head2 init --vob <path>\n+\n+Initialize the current working directory as a git repository and setup the\n+configuration for the B<VOB> to be imported.  The results of this are a .git\n+directory within the current working directory.\n+\n+=head2 fetch\n+\n+Retrieve the clearcase history for the previously configured B<VOB>.  The first\n+time this is run, the entire history is logged using the following clearcase\n+format (cleartool man fmt_ccase):\n+\n+%o%m#1#%Nd#1#%Fu <%u>#1#%En#1#%Vn#1#%[slink_text]p#1#%Nc\\n\n+\n+  %o  | Operation kind (checkin, lock, mkelem, and so on)\n+  %m  | Object kind (version, derived object, and so on)\n+      | (modifiers: K)\n+  %Nd | (Numeric) Date and time in numeric form -- yyyymmdd.time\n+      | (time reported in 24-hour format).\n+  %Fu | Full name of the user. This information is taken from\n+      | the password database.\n+  %u  | User/group information associated with the object's\n+      | creation event (modifiers: F, G, L); see also %[owner]p and\n+      | %[group]p.\n+  %En | Element name: For a file system object, its standard file\n+      | or element name, or its path name; for a type object, its\n+      | name.\n+  %Vn | Version ID: For a version or derived object, the version\n+      | ID; for other objects, the null string.\n+  %Nc | comment string. No newline. Do not append a newline character to\n+      | the comment string.\n+ %[slink_text]p | VOB symbolic links | Target of symbolic link, as\n+                |                    | displayed by cleartool ls.\n+\n+Subsequent runs only pull in the changes since the last run.\n+\n+=head2 commit\n+\n+This generates git commits from the clearcase event history fetched via the\n+B<fetch> command.\n+\n+=head1 OPTIONS\n+\n+=head2 --vob <path>\n+\n+Specify the path to the B<VOB>.\n+\n+=head2 --verbose\n+\n+Emit extra and informational messages during normal operation.\n+\n+=head2 --savetemps\n+\n+Do not discard temporary files during the import.\n+\n+=head1 AUTHOR\n+\n+Jason Woodward <woodwardj@jaos.org>\n+\n+=cut\n+\n+sub usage\n+{\n+print \"Usage:\n+  cc-base-import [command] [option(s)]\n+\n+  commands: init --vob <path> | fetch | commit\n+\n+  options: [--verbose | --savetemps]\n+\\n\";\n+}\n+\n+sub cc_time_to_epoch\n+{\n+\tmy ($date) = @_;\n+\tmy ($year,$mon,$day,$dot,$hour,$min,$sec) = unpack (\"A4A2A2aA2A2A2\", $date);\n+\treturn mktime ($sec, $min, $hour, $day, $mon - 1, $year - 1900);\n+}\n+\n+\n+sub _increment_by_one_second\n+{\n+\tmy ($t) = @_;\n+\tif ($t && $t =~ m/.*?([0-9][0-9])$/) {\n+\t\tmy $sec = $1;\n+\t\t$sec++;\n+\t\t$t =~ s/$1$/$sec/;\n+\t}\n+\treturn $t;\n+}\n+\n+sub gen_cc_history_journal\n+{\n+\tmy ($config) = @_;\n+\n+\tlockme ($config);\n+\n+\tchdir ($config->{vob});\n+\n+\tmy $redir_opt = '>';\n+\t# caching, use last commit time\n+\tmy $last_commit_ftimetstamp = $config->{last_commit_ftimestamp};\n+\t# try to only fetch what we haven't already\n+\tif ( -e $config->{journal_file}) {\n+\t\tmy $data;\n+\t\topen (my $fh, '<', $config->{journal_file}) or die \"Failed to open $config->{journal_file}: $!\";\n+\t\tseek ($fh, -512, 2);  # seek to the end\n+\t\tread $fh, $data, 512; # read a chunk\n+\t\tclose ($fh);\n+\t\tmy @timestamps = ($data =~ m/#([0-9]+\\.[0-9]+)#/gs); # extract timestamps\n+\t\tif ($timestamps[-1]) { # use the last one... this way we optimize our fetch time\n+\t\t\t($last_commit_ftimetstamp = $timestamps[-1]) =~ s/\\./T/;\n+\t\t\t$redir_opt = '>>';\n+\t\t}\n+\t}\n+\n+\t$last_commit_ftimetstamp = _increment_by_one_second($last_commit_ftimetstamp);\n+\n+\tmy $since_opt = $last_commit_ftimetstamp ? \"-since $last_commit_ftimetstamp\" : '';\n+\tprint \"Fetching clearcase events\", $since_opt ? \" since $last_commit_ftimetstamp\" : '',\" (this may take a while)...\\n\";\n+\t$@ = undef;\n+\teval {\n+\t\tmy $fmt = '%o%m#1#%Nd#1#%Fu <%u>#1#%En#1#%Vn#1#%[slink_text]p\\\\n';\n+\t\texeccmd ( \"cleartool lsh -fmt '$fmt' -recurse $since_opt $config->{vob} | perl -e 'print reverse <>' $redir_opt $config->{journal_file}\" );\n+\t};\n+\tif ($@) {\n+\t\tchdir ($config->{dest});\n+\t\tdie \"$@\";\n+\t}\n+\n+\texeccmd (\"echo \\$(wc -l $config->{journal_file}) events logged\");\n+\n+\tchdir ($config->{dest});\n+\tunlockme ($config);\n+}\n+\n+\n+sub execcmd\n+{\n+\tmy $opts;\n+\t# options hashref passed at the end\n+\tif (ref $_[-1] eq 'HASH') {\n+\t\t$opts = pop;\n+\t}\n+\n+\tmy ($w, $r, $e); # write fd, read fd, and err fd\n+\tmy $pid = open3($w, $r, $e, @_);\n+\twaitpid ($pid, 0);\n+\tmy $exit_status = $? >> 8; # get child exit status\n+\tmy $output;\n+\tread ($e ? $e : $r, $output, 512); # cleartool writes errors to stdout instead of stderr\n+\n+\tunless ($opts->{nooutput}) {\n+\t\tprint $output;\n+\t}\n+\tif (exists $opts->{saveout} ) { # pass back output if we need it captured\n+\t\t${$opts->{saveout}} = $output;\n+\t}\n+\n+\tif ($exit_status != 0) {\n+\t\tdie \"$output\"; # raise\n+\t}\n+\treturn $exit_status;\n+}\n+\n+sub commit\n+{\n+\tmy ($config, $commit, $fds) = @_;\n+\n+\t# full paths\n+\tmy $dest      = \"dest_tmp.$$\";\n+\tmy $src       = File::Spec->join ($config->{vob}, $commit->{file});\n+\tmy $src_ver   = $src . '@@' . $commit->{ver}; # XXX portability?  Does windows expose file@@version?\n+\n+\tmy $mode      = '100644';  # normal file, see git-fast-import(1)\n+\tmy $branch    = $commit->{branch} || 'master';\n+\n+\t# duplex if necessary\n+\tmy $_printf = sub {\n+\t\tmy ($fds, $format, @args) = @_;\n+\t\tfor my $fd (@$fds) {\n+\t\t\tprintf $fd $format, @args;\n+\t\t}\n+\t};\n+\n+\t$@ = undef;\n+\teval {\n+\t\texeccmd ( \"cleartool get -to '$dest' \\\"$src_ver\\\"\" ) if $commit->{event} !~ m/mkslinksymbolic/;\n+\t};\n+\tif ($@) { # catch\n+\t\tunlink $dest if ( -e $dest );\n+\t\tif ($@ =~ m/cleartool: .*No such device or address/) {\n+\t\t\twarn \"*** ignoring missing clearcase data\" if $config->{verbose};\n+\t\t\treturn;\n+\t\t}\n+\t\tunlockme ($config);\n+\t\tdie \"UNHANDLED: $@\\n$commit->{rawline}\\n\"; # raise\n+\t}\n+\n+\t# here we generate the git fast-export format\n+\t$config->{mark}++;\n+\t$_printf->($fds, \"blob\\n\");\n+\t$_printf->($fds, \"mark :%d\\n\", $config->{mark}++);\n+\n+\t# file data\n+\tif ($commit->{event} =~ m/mkslinksymbolic/) {\n+\t\t$mode = '120000'; # git symlink mode, see git-fast-import(1)\n+\t\t$_printf->($fds, \"data %d\\n\", length $commit->{symlink});\n+\t\t$_printf->($fds, \"%s\\n\", $commit->{symlink});\n+\t\t$branch = $config->{last_branch}; # symlinks do not have branch entries in clearcase, reuse prior branch\n+\t} else {\n+\t\t$mode = '100755' if ( -x $dest ); # git executable, see git-fast-import(1)\n+\t\tmy $data_size = (stat $dest)[7];\n+\t\t$_printf->($fds, \"data %d\\n\", $data_size);\n+\t\t{ open (my $fh, '<', $dest) or die $!; $_printf->($fds, \"%s\", $_) while (<$fh>); close($fh); $_printf->($fds,\"\\n\"); }\n+\t\tunlink $dest if ( -e $dest );\n+\t}\n+\n+\t# commit info\n+\t$_printf->($fds, \"progress %s: %s @@ %s\\n\", $commit->{date}, $commit->{file}, $commit->{ver});\n+\t$_printf->($fds, \"commit refs/heads/%s\\n\", $branch);\n+\t$_printf->($fds, \"mark :%d\\n\", $config->{mark});\n+\t$_printf->($fds, \"author %s %d -0500\\n\", $commit->{user}, cc_time_to_epoch($commit->{date}));\n+\t$_printf->($fds, \"committer %s %d -0500\\n\", $commit->{user}, cc_time_to_epoch($commit->{date}));\n+\n+\t# commit message\n+\tmy $msg;\n+\texeccmd( \"cleartool desc -fmt '%Nc' \\\"$src_ver\\\"\", { nooutput => 1, saveout=> \\$msg } );\n+\tif ($msg) {\n+\t\tmy $msg_size = length $msg;\n+\t\t$_printf->($fds, \"data %d\\n\", $msg_size);\n+\t\t$_printf->($fds, \"%s\\n\", $msg);\n+\t} else {\n+\t\t$_printf->($fds, \"data 0\\n\");\n+\t}\n+\n+\t# parent commit\n+\tmy $from_mark = $config->{branch_marks}->{$branch};\n+\tif ($from_mark) {\n+\t\t$_printf->($fds, \"from :%d\\n\", $from_mark);\n+\t} else { # ask git if we have an existing sha1 for this branch to parent our commit on\n+\t\tundef $@;\n+\t\teval {\n+\t\t\tmy $from_ref;\n+\t\t\texeccmd (\"git show-ref -s $branch\", { nooutput => 1, saveout => \\$from_ref } );\n+\t\t\tchomp $from_ref;\n+\t\t\t$from_ref =~ s/\\n.*//ms; # in case we get multiple lines\n+\t\t\t$_printf->($fds, \"from %s\\n\", $from_ref);\n+\t\t};\n+\t\tif ($@) {\n+\t\t\t# if we have gotten here, we don't know where to parent from a previous branch\n+\t\t\t# which means this must be the first commit of the branch.  We need to set\n+\t\t\t# from to be the tip of master\n+\t\t\tif ($branch ne 'master') {\n+\t\t\t\tmy $master_mark = $config->{branch_marks}->{master};\n+\t\t\t\tif ($master_mark) {\n+\t\t\t\t\t$_printf->($fds, \"from :%d\\n\", $master_mark);\n+\t\t\t\t} else {\n+\t\t\t\t\tundef $@;\n+\t\t\t\t\teval {\n+\t\t\t\t\t\tmy $from_ref;\n+\t\t\t\t\t\texeccmd (\"git show-ref -s master\", { nooutput => 1, saveout => \\$from_ref } );\n+\t\t\t\t\t\tchomp $from_ref;\n+\t\t\t\t\t\t$from_ref =~ s/\\n.*//ms; # in case we get multiple lines\n+\t\t\t\t\t\t$_printf->($fds, \"from %s\\n\", $from_ref);\n+\t\t\t\t\t};\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\t# final stanza\n+\t$_printf->($fds, \"M %d :%d %s\\n\\n\", $mode, $config->{mark}-1, $commit->{file});\n+\n+\t# store our \"mark\" counter for parent reference of future commits in the current branch\n+\t$config->{branch_marks}->{$branch} = $config->{mark};\n+\t# store this for symlink entries\n+\t$config->{last_branch} = $branch;\n+\n+\tlog_last_event ($config, $commit->{date});\n+}\n+\n+sub walk_journal\n+{\n+\tmy ($config) = @_;\n+\n+\topen (my $journal,'<', $config->{journal_file}) or die \"Nothing to commit. Try running fetch.\\n\";\n+\n+\tmy $journal_loc;\n+\tif ( -e $config->{journal_file_loc} ) {\n+\t\topen( $journal_loc, '+<', $config->{journal_file_loc}) or die $!; # r/w\n+\t\tread $journal_loc, my $seek, 64;\n+\t\tseek $journal, $seek, 0 if $seek;\n+\t} else {\n+\t\topen( $journal_loc, '>', $config->{journal_file_loc}) or die $!; # create if necessary\n+\t}\n+\tselect((select($journal_loc), $|=1)[0]); # unbuffered\n+\n+\tlockme ($config);\n+\n+\t# open a git fast-import pipe\n+\tmy @fds;\n+\topen ($fds[0], '| git fast-import --quiet') or die $!;\n+\tselect((select($fds[0]), $|=1)[0]); # unbuffered\n+\tif ($config->{savetemps}) {\n+\t\t# store what we write to the pipe in a local cache file\n+\t\twarn \"*** Saving fast-import data to $config->{fast_import}\\n\" if $config->{verbose};\n+\t\topen ($fds[1], '>>', $config->{fast_import}) or die $!;\n+\t\tselect((select($fds[1]), $|=1)[0]); # unbuffered\n+\t}\n+\n+\twhile (my $line = <$journal>) {\n+\t\tchomp $line;\n+\t\tnext if $line =~ m/^\\s*$/;\n+\t\t$line =~ s/\\015//;\n+\n+\t\tmy $commit = {};\n+\t\t@{$commit}{ qw/event date user file ver symlink/ } = split '#1#', $line;\n+\n+\t\t# only want specific events\n+\t\tif (!grep { m/^(?:(checkinversion|mkslinksymbolic|checkinderived))/; } ($commit->{event})) {\n+\t\t\tlog_last_event ($config, $commit->{date});\n+\t\t\twarn \"*** Discarding event $commit->{event}\\n\" if $config->{verbose};\n+\t\t\tnext;\n+\t\t}\n+\n+\t\t# forget about lost+found\n+\t\tif ($commit->{file} =~ m/lost\\+found/) {\n+\t\t\twarn \"*** Discarding lost+found file: $commit->{file}\\n\" if $config->{verbose};\n+\t\t\tnext;\n+\t\t}\n+\n+\t\t# branch logic\n+\t\tif ($commit->{event} =~ m/checkinversion/) { # no version on symlink entry\n+\t\t\tif ($commit->{ver} =~ m/\\/main\\/(.*?)\\/[0-9]$/) {\n+\t\t\t\t$commit->{branch} = $1; # store the branch\n+\t\t\t}\n+\t\t}\n+\n+\t\t# cleanup\n+\t\t$commit->{formatted_date} = cc_time_to_epoch ($commit->{date});\n+\t\t#$commit->{file} =~ s|\\$|\\\\\\$|g;\n+\t\t$commit->{file} =~ s|\\`|\\\\\\`|g; # ` unwanted in file name\n+\t\t$commit->{file} =~ s|$config->{vob}/*||;\n+\t\t$commit->{rawline} = $line;\n+\n+\t\tcommit ($config, $commit, \\@fds);\n+\t\tseek $journal_loc, 0, 0;\n+\t\tprint $journal_loc tell $journal;\n+\n+\t}\n+\n+\t# setup HEAD\n+\tif ($config->{mark} > 0) {\n+\t\tmy $head_from;\n+\t\tif ($config->{branch_marks}->{'master'}) {\n+\t\t\t$head_from = sprintf (\"from :%d\\n\\n\", $config->{branch_marks}->{'master'});\n+\t\t} else { # load master ref\n+\t\t\tmy $from_ref;\n+\t\t\texeccmd (\"git show-ref -s master\", { nooutput => 1, saveout => \\$from_ref } );\n+\t\t\tchomp $from_ref;\n+\t\t\t$from_ref =~ s/\\n.*//ms; # in case we get multiple lines\n+\t\t\t$head_from = sprintf (\"from %s\\n\\n\", $from_ref);\n+\t\t}\n+\t\tfor my $fd (@fds) {\n+\t\t\tprintf $fd \"reset refs/heads/master\\n\";\n+\t\t\tprintf $fd $head_from;\n+\t\t}\n+\t}\n+\n+\tclose ($journal);\n+\tclose ($journal_loc);\n+\tclose ($fds[0]);\n+\tclose ($fds[1]) if $fds[1];\n+\tunlink ($config->{journal_file}) unless $config->{savetemps};\n+\tunlink ($config->{journal_file_loc}) unless $config->{savetemps};\n+\tunlockme ($config);\n+}\n+\n+# we store the timestamp of the last clearcase event so later we can use \"-since\"\n+sub log_last_event\n+{\n+\tmy ($config, $timestamp) = @_;\n+\treturn if !$timestamp;\n+\n+\topen (my $logfh, '>', $config->{last_commit_file}) or die \"Failed to open $config->{last_commit_file} or writing: $!\";\n+\tprint $logfh $timestamp;\n+\tclose ($logfh);\n+}\n+\n+sub cc_base_import_init\n+{\n+\tmy ($config) = @_;\n+\n+\tif (!$config->{vob}) {\n+\t\tprintf \"You must specify --vob with init\\n\";\n+\t\texit (-1);\n+\t}\n+\n+\tif ( -d '.git' ) {\n+\t\tdie \"This directory already has a .git subdirectory!\\n\";\n+\t}\n+\n+\texeccmd ('git init');\n+\n+\tmy $config_file = File::Spec->join ($config->{dest}, '.git', 'cc-base-import.config');\n+\topen (my $config_fh, '>', $config_file) or die \"Failed to write $config_file: $!\";\n+\tprint $config_fh $config->{vob};\n+\tclose ($config_fh);\n+\n+}\n+\n+sub lockme   { my $config = shift; mkdir $config->{lock_file} or die \"Process already running\"; }\n+sub unlockme { my $config = shift; rmdir $config->{lock_file} or die \"Unable to remove lock directory\"; }\n+\n+sub load_config\n+{\n+\tmy ($config) = @_;\n+\tmy $f;\n+\tmy $err = \"Unable to read configuration, perhaps you want to run 'init' first?\\n\";\n+\n+\t$config->{git_dir}          = File::Spec->catdir ($config->{dest},  '.git');\n+\t$config->{last_commit_file} = File::Spec->join ($config->{git_dir}, 'cc-base-import.last-commit-time');\n+\t$config->{journal_file}     = File::Spec->join ($config->{git_dir}, 'cc-base-import.journal');\n+\t$config->{journal_file_loc} = File::Spec->join ($config->{git_dir}, 'cc-base-import.journal.location');\n+\t$config->{config_file}      = File::Spec->join ($config->{git_dir}, 'cc-base-import.config');\n+\t$config->{lock_file}        = File::Spec->join ($config->{git_dir}, 'cc-base-import.lock');\n+\t$config->{fast_import}      = File::Spec->join ($config->{git_dir}, 'cc-base-import.fast-import');\n+\n+\tif ( ! -d '.git' && ! -e $config->{config_file}) {\n+\t\tprintf $err; exit (-1);\n+\t}\n+\n+\topen ($f, '<', $config->{config_file}) or die \"Failed to open $config->{config_file}: $!\";\n+\tread $f, $config->{vob}, 4096;\n+\tclose ($f);\n+\n+\tif (!$config->{vob}) {\n+\t\tprintf $err; exit (-1);\n+\t}\n+\n+\tchomp $config->{vob};\n+\n+\tif ( -e $config->{last_commit_file} ) {\n+\t\topen ($f, '<', $config->{last_commit_file}) or die \"Failed to open $config->{last_commit_file}: $!\";\n+\t\tread $f, $config->{last_commit_timestamp}, 4096;\n+\t\tclose ($f);\n+\t\tchomp $config->{last_commit_timestamp};\n+\t\t($config->{last_commit_ftimestamp} = $config->{last_commit_timestamp}) =~ s/\\./T/g;\n+\t}\n+\n+}\n+\n+\n+# The main meat of this utility... parse the arguments and dispatch\n+\n+my $config = {\n+\t'dest'        => File::Spec->rel2abs ( File::Spec->curdir () ),\n+\t'mark'        => 0,\n+\t'branch_mark' => {},\n+};\n+my $command = shift;\n+my $result = GetOptions (\n+\t\"vob=s\" => \\$config->{vob},\n+\t\"verbose\" => \\$config->{verbose},\n+\t\"savetemps\" => \\$config->{savetemps},\n+);\n+if (!$result || !$command) {\n+\tusage ();\n+\texit (-1);\n+}\n+\n+my $commands = {\n+\t'init'    => sub {\n+\t\tcc_base_import_init ($config);\n+\t},\n+\t'fetch'  => sub {\n+\t\tload_config ($config);\n+\t\tgen_cc_history_journal ($config);\n+\t},\n+\t'commit'  => sub {\n+\t\tload_config ($config);\n+\t\twalk_journal ($config);\n+\t},\n+\t'unlock'  => sub {\n+\t\tload_config ($config);\n+\t\tunlockme ($config);\n+\t},\n+};\n+\n+if (! exists $commands->{$command} ) {\n+\tusage ();\n+\texit (-1);\n+}\n+$SIG{INT} = sub { unlockme ($config); exit (-1); };\n+$commands->{$command}->();\n+\ndiff --git a/contrib/cc-base-import/cc-base-import.txt b/contrib/cc-base-import/cc-base-import.txt\nnew file mode 100644\nindex 0000000..0d83889\n--- /dev/null\n+++ b/contrib/cc-base-import/cc-base-import.txt\n@@ -0,0 +1,111 @@\n+cc-base-import(1)\n+=================\n+\n+NAME\n+----\n+cc-base-import - Import a ClearCase VOB into git\n+\n+SYNOPSIS\n+--------\n+[verse]\n+cc-base-import [command] [option(s)]\n+\n+commands: init --vob <path> | fetch | commit\n+\n+options: [--verbose | --savetemps]\n+\n+example:\n+\n+[verse]\n+$ mkdir /tmp/project\n+$ cd /tmp/project\n+$ git cc-base-import init --vob /view/view_main/vobs/vob1 --verbose\n+Initialized empty Git repository in /tmp/project/.git/\n+$ git cc-base-import fetch --verbose\n+Fetching clearcase events\n+$ git cc-base-import commit\n+... [generates git commits from clearcase events]\n+ $ git checkout master\n+ $ ls\n+...\n+\n+DESCRIPTION\n+-----------\n+\n+This utility walks the history of a ClearCase VOB creating equivalent\n+commits in git. Branch names, commit messages, and author information is\n+preserved.\n+\n+The only requirement is the cleartool utility must be present in the\n+current PATH.\n+\n+*NOTE* this does not work with ClearCase UCM, only base ClearCase.\n+\n+Error Handling\n+~~~~~~~~~~~~~~\n+Certain data loss messages are currently ignored, such as the following:\n+\n+'cleartool: Error: Operation \"file copy\" failed: No such device or address.'\n+\n+Other errors are not, such as:\n+\n+'cleartool: Error: Operation \"file copy\" failed: Input/output error.'\n+\n+Deficiencies\n+~~~~~~~~~~~~\n+\n+There is no attempt to handle tracking branch renames in clearcase.\n+\n+File deletions are also not handled.  Some info can be gleamed from the directory\n+history but that information is not enforced and can be overridden.  This is\n+also true for file moves.\n+\n+A suggested strategy is to run the conversion and enforce no file moves,\n+deletions, or branch renames during a set window.\n+\n+Merge arrows and related clearcase meta data are not preserved in the conversion,\n+only the file contents.\n+\n+Labels are not yet accounted for.  Since labels do not represent an atomic operation\n+in clearcase, there is not a general way to represent a git tag as a clearcase label.\n+\n+COMMANDS\n+--------\n+\n+init --vob <path>\n+~~~~~~~~~~~~~~~~~\n+\n+Initialize the current working directory as a git repository and setup\n+the configuration for the VOB to be imported. The results of this are a\n+.git directory within the current working directory.\n+\n+fetch\n+~~~~~\n+\n+Retrieve the clearcase history for the previously configured VOB. The\n+first time this is run, the entire history is logged.\n+\n+commit\n+~~~~~~\n+\n+This generates git commits from the clearcase event history fetched via\n+the fetch command.\n+\n+OPTIONS\n+-------\n+--vob:: <path>\n+  Specify the path to the VOB.\n+\n+--verbose::\n+  Emit extra and informational messages during normal operation.\n+\n+--savetemps::\n+  Do not discard temporary files during the import.\n+\n+AUTHOR\n+------\n+Jason Woodward <woodwardj@jaos.org>\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\n-- \n2.1.3\n"}]}