├── .gitignore ├── LICENSE ├── README.markdown └── darcs-to-git /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2014 Steve Purcell, http://www.sanityinc.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | darcs-to-git 2 | ============ 3 | 4 | Converts a Darcs repository into a Git repository. Supports 5 | incremental updates, i.e., you can pull new patches from the source 6 | repository or import a large repository in steps. 7 | 8 | Usage 9 | ----- 10 | 11 | (Use `darcs-to-git --help` to display the latest usage instructions.) 12 | 13 | 1. Create an *empty* directory that will become the new git repository 14 | 2. From inside that directory, run this program, passing the location 15 | of the local source darcs repo as a parameter 16 | 17 | The program will git-init the empty directory, and migrate all patches 18 | in the source darcs repo into commits in that repository. 19 | 20 | Thereafter, incremental patch conversion from the same source repo is 21 | possible by repeating step 2. 22 | 23 | Options 24 | ------- 25 | 26 | * `--patches N`: only import `N` patches. 27 | 28 | * `--email-address ADDRESS`: `darcs-to-git` tries to reconstruct the 29 | email address from the darcs patch. In cases this is not possible, 30 | a default will be picked by Git. This is usually the one in 31 | `~/.gitconfig`. This option allows you to specify another default 32 | (without having to to modify `~/.gitconfig.`) 33 | 34 | * `--list-authors`: Outputs a list of authors in the source 35 | repository and how they will appear in the git repository and 36 | quits. The output will be lines like this: 37 | 38 | ``` 39 | Jane@example.com: Jane 40 | ``` 41 | 42 | This means that the darcs author `Jane@example.com` will be 43 | translated to git-author `Jane` with email address 44 | `Jane@example.com`. You can use the output of this command as a 45 | starting point for the input for `--author-map`. 46 | 47 | * `--author-map FILENAME`: Allows translations from darcs committer 48 | name to Git committer name. The input is a YAML map. For an 49 | example see the output of `--list-authors`. The author map will be 50 | stored in the repository and will be re-used for future imports. 51 | 52 | 53 | Known issues 54 | ------------ 55 | 56 | When `darcs-to-git` pulls a conflicting patch it will revert the state 57 | of the repository to the state before the conflict. **This will also 58 | remove any local changes to your repository, including git commits!** 59 | You should therefore not commit to the branch you import to, but 60 | instead work in a different branch. You can rename your master branch 61 | after import using: 62 | 63 | $ git branch -m darcs_import 64 | 65 | `darcs-to-git` creates a full copy of the original repository in addition to the Git repository; 66 | this can lead to considerable space usage. You can save space by treating the copied Darcs 67 | repository [as a branch](http://wiki.darcs.net/BestPractices#how-to-create-a-branch) by 68 | running 69 | 70 | $ darcs optimize --relink --sibling /old-repo/dir 71 | 72 | inside the new repository. 73 | 74 | Acknowledgements 75 | ---------------- 76 | 77 | Written and maintained by Steve Purcell, with some improvements by 78 | Thomas Schilling, Jonathon Mah and others. 79 | 80 | 81 |
82 | 83 | [💝 Support this project and my other Open Source work via Patreon](https://www.patreon.com/sanityinc) 84 | 85 | [💼 LinkedIn profile](https://uk.linkedin.com/in/stevepurcell) 86 | 87 | [✍ sanityinc.com](http://www.sanityinc.com/) 88 | 89 | -------------------------------------------------------------------------------- /darcs-to-git: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | ## 4 | ## Author: Steve Purcell, http://www.sanityinc.com/ 5 | ## 6 | ## Further info: 7 | ## http://www.sanityinc.com/articles/converting-darcs-repositories-to-git 8 | ## 9 | ## Obtain the latest version of this software here: 10 | ## 11 | ## https://github.com/purcell/darcs-to-git 12 | ## http://git.sanityinc.com/ 13 | ## 14 | ## Please submit bug reports and patches via Github if possible: 15 | ## 16 | ## https://github.com/purcell/darcs-to-git/issues 17 | ## 18 | ## or by email to the author if necessary. 19 | ## 20 | ## 21 | 22 | # XXX: make backwards compatible 23 | # TODO: import parallel darcs repos as git branches, identifying branch points 24 | # TODO: use default repo if none was supplied 25 | # TODO: handle *-darcs-backupN files? 26 | 27 | if RUBY_VERSION >= "1.9" 28 | require 'shellwords' 29 | else 30 | module Shellwords 31 | def self.shellescape(str) 32 | return "''" if str.empty? 33 | str.gsub(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1").gsub(/\n/, "'\n'") 34 | end 35 | end 36 | end 37 | 38 | require 'ostruct' 39 | require 'rexml/document' 40 | require 'optparse' 41 | require 'yaml' 42 | require 'pathname' 43 | require 'iconv' if RUBY_VERSION < "2.0.0" 44 | require 'fileutils' 45 | require 'open-uri' 46 | 47 | # Explicitly setting a time zone would cause darcs to only output in 48 | # that timezone hence we couldn't get the actual patch TZ 49 | # ENV['TZ'] = 'GMT0' 50 | 51 | GIT_PATCHES = ".git/darcs_patches" 52 | DEFAULT_AUTHOR_MAP_FILE = ".git/darcs_author_substitutions" 53 | 54 | # ------------------------------------------------------------------------------- 55 | # Usage info and argument parsing 56 | # ------------------------------------------------------------------------------- 57 | 58 | OPTIONS = { :default_author => 'none', 59 | :default_email => 'none', 60 | :list_authors => false, 61 | :author_map => nil, 62 | :verbose => false, 63 | :quiet => false, 64 | :do_checks => true, 65 | :clean_commit_messages => false, 66 | :num_patches => nil } 67 | options = OptionParser.new do |opts| 68 | opts.banner = <<-end_usage 69 | Creates git repositories from darcs repositories 70 | 71 | usage: darcs-to-git DARCSREPO [options] 72 | 73 | 74 | 1. Create an *empty* directory that will become the new git repository 75 | 2. From inside that directory, run this program, passing the location 76 | of the source darcs repo as a parameter 77 | 78 | The program will git-init the empty directory, and migrate all patches 79 | in the source darcs repo into commits in that repository. 80 | 81 | Thereafter, incremental patch conversion from the same source repo is 82 | possible by repeating step 2. 83 | 84 | If DARCSREPO is local, its contents will be compared with those of the 85 | git repo after conversion finishes, as a sanity check for the 86 | conversion process. 87 | 88 | NOTE: In case of duplicate tags, the latest will take precedence. 89 | If you really need to, you can manually identify the patch and use 90 | \"git tag -f \". 91 | 92 | OPTIONS 93 | 94 | end_usage 95 | opts.on('--default-author NAME', 96 | "Set the author name used when darcs patch has no explicit author") do |m| 97 | OPTIONS[:default_author] = m 98 | end 99 | opts.on('--default-email ADDRESS', 100 | "Set the email address used when no explicit address is given") do |m| 101 | OPTIONS[:default_email] = m 102 | end 103 | opts.on('--list-authors', 104 | "List all unique authors in source repo and quit.") do |m| 105 | OPTIONS[:list_authors] = m 106 | end 107 | opts.on('--author-map FILE', 108 | "Supply a YAML file that maps committer names to canonical author names") do |f| 109 | OPTIONS[:author_map] = f 110 | end 111 | opts.on('--patches [N]', OptionParser::DecimalInteger, 112 | "Only pull N patches.") do |n| 113 | abort opts.to_s unless n >= 0 114 | OPTIONS[:num_patches] = n 115 | end 116 | opts.on('--verbose', 117 | "Show executed commands and other internal information") do |n| 118 | OPTIONS[:verbose] = true 119 | end 120 | opts.on('--no-verbose', 121 | "Don't show status update after every imported patch (bit faster) ") do |n| 122 | OPTIONS[:quiet] = true 123 | end 124 | opts.on('--no-checks', 125 | "Don't check repository consistency after every imported patch, only at start and finish of pull (faster) ") do |n| 126 | OPTIONS[:do_checks] = false 127 | end 128 | opts.on('--clean-commit-messages', 129 | "Don't note darcs hashes in git commit messages (not recommended)") do |n| 130 | OPTIONS[:clean_commit_messages] = true 131 | end 132 | opts.on('--version', "Output version information and exit") do 133 | puts <<-EOF 134 | darcs-to-git 0.2 135 | 136 | Copyright (c) 2009-#{Time.now.year} Steve Purcell, http://www.sanityinc.com/ 137 | 138 | License MIT: 139 | This is free software: you are free to change and redistribute it. 140 | There is NO WARRANTY, to the extent permitted by law. 141 | EOF 142 | exit 143 | end 144 | 145 | opts.on('-h', '--help', "Show this message") do 146 | puts opts.to_s 147 | exit 148 | end 149 | end 150 | options.parse! 151 | 152 | SRCREPO = ARGV[0] 153 | if SRCREPO.nil? 154 | abort options.to_s 155 | end 156 | 157 | 158 | # ------------------------------------------------------------------------------- 159 | # Utilities 160 | # ------------------------------------------------------------------------------- 161 | def run(*args) 162 | puts "Running: #{args.inspect}" if OPTIONS[:verbose] 163 | system(*args) || raise("Failed to run: #{args.inspect}") 164 | end 165 | 166 | # cf. Paul Battley, http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/ 167 | def validate_utf8(s) 168 | if defined? Iconv 169 | Iconv.iconv('UTF-8//IGNORE', 'UTF-8', (s + ' ') ).first[0..-2] 170 | else 171 | # Force conversion and sanitization by encoding to UTF-16, then back to UTF-8 172 | s.dup.encode('UTF-16BE', :invalid => :replace, :undef => :replace, :replace => ""). 173 | encode('UTF-8', :invalid => :replace, :undef => :replace, :replace => "") 174 | end 175 | end 176 | 177 | def output_of(*args) 178 | puts "Running: #{args.inspect}" if OPTIONS[:verbose] 179 | output = IO.popen(args.map {|a| Shellwords.shellescape(a) }.join(' '), 'r') { |p| p.read } 180 | if $?.exitstatus == 0 181 | return validate_utf8(output) 182 | else 183 | raise "Failed to run: #{args.inspect}" 184 | end 185 | end 186 | 187 | class Symbol 188 | def to_proc() lambda { |o| o.send(self) } end 189 | end 190 | 191 | class String 192 | def darcs_unescape 193 | # darcs uses '[_\hh_]' to quote non-ascii characters where 'h' is 194 | # a hexadecimal. We translate this to '=hh' and use ruby's unpack 195 | # to do replace this with the proper byte. 196 | gsub(/\[\_\\(..)\_\]/) { |x| "=#{$1}".unpack("M*")[0] } 197 | end 198 | end 199 | 200 | 201 | # ------------------------------------------------------------------------------- 202 | # Map darcs authors to git authors 203 | # ------------------------------------------------------------------------------- 204 | class AuthorMap < Hash 205 | attr_accessor :default_email 206 | attr_accessor :default_author 207 | 208 | def self.load(filename) 209 | new.merge(YAML.load_file(filename)) 210 | end 211 | 212 | # gives the name and email 213 | def [](author) 214 | name_and_email(super || author) 215 | end 216 | 217 | private 218 | 219 | def name_and_email(author) 220 | case author 221 | when /^\s*(\S.*?)\s*\<(\S+@\S+?)\>\s*$/ 222 | [$1, $2] 223 | when /^\s*\?\s*$/ 224 | email = $1 225 | [email.split('@').first, email] 226 | when '' 227 | [default_author, default_email] 228 | else 229 | [author, default_email] 230 | end 231 | end 232 | end 233 | 234 | # ------------------------------------------------------------------------------- 235 | # Storing a history of related darcs and git commits 236 | # ------------------------------------------------------------------------------- 237 | 238 | class CommitHistory 239 | def initialize(patch_file_name) 240 | @patch_file_name = patch_file_name 241 | @darcs_patches_in_git = {} 242 | if File.exist?(patch_file_name) 243 | @darcs_patches_in_git = YAML.load_file(patch_file_name) 244 | unless @darcs_patches_in_git.is_a?(Hash) 245 | raise "yaml hash not found in #{patch_file_name}" 246 | end 247 | else 248 | # TODO: consider doing this unconditionally, since that 249 | # might allow merging between repositories created with darcs-to-git 250 | fill_from_darcs_hash_comments 251 | end 252 | end 253 | 254 | def record_git_commit(commit_id, identifier) 255 | # using one file per darcs patch would be an incredible waste of space 256 | # on my system one file takes up 4K even if only a few bytes are in it 257 | # hence we just use a simple YAML hash 258 | @darcs_patches_in_git[identifier] = commit_id 259 | File.open(@patch_file_name, 'w') do |f| 260 | YAML.dump(@darcs_patches_in_git, f) 261 | end 262 | end 263 | 264 | def find_git_commit(is_tag, git_tag_name, identifier) 265 | @darcs_patches_in_git[identifier] || 266 | if is_tag 267 | (output_of("git", "tag", "-l") rescue "").split(/\r?\n/).include?(git_tag_name) && 268 | output_of("git", "rev-list", "--max-count=1", "tags/#{git_tag_name}").strip 269 | end 270 | end 271 | 272 | def empty_repo? 273 | !system("git rev-parse --verify HEAD >/dev/null 2>&1") 274 | end 275 | 276 | private 277 | 278 | def fill_from_darcs_hash_comments 279 | return if empty_repo? 280 | Array(output_of("git", "log", "--grep=darcs-hash:", "--no-color").split(/^commit /m)[1..-1]).each do |entry| 281 | commit_id, identifier = entry.scan(/^([a-z0-9]+$).*darcs-hash:(.*?)$/sm).flatten 282 | record_git_commit(commit_id, identifier) 283 | end 284 | end 285 | end 286 | 287 | # ------------------------------------------------------------------------------- 288 | # Reading darcs patches and applying them to a git repo 289 | # ------------------------------------------------------------------------------- 290 | 291 | class DarcsPatch 292 | attr_accessor :source_repo, :author, :date, :inverted, :identifier, :name, :is_tag, :git_tag_name, :comment 293 | attr_reader :git_author_name, :git_author_email 294 | 295 | def initialize(source_repo, patch_xml) 296 | self.source_repo = source_repo 297 | self.author = patch_xml.attribute('author').value.darcs_unescape 298 | self.date = darcs_date_to_git_date(patch_xml.attribute('date').value, 299 | patch_xml.attribute('local_date').value) 300 | self.inverted = (patch_xml.attribute('inverted').to_s == 'True') 301 | self.identifier = patch_xml.attribute('hash').to_s 302 | self.name = patch_xml.get_elements('name').first.get_text.value.darcs_unescape rescue 'Unnamed patch' 303 | self.comment = patch_xml.get_elements('comment').first.get_text.value.darcs_unescape rescue nil 304 | if (self.is_tag = (self.name =~ /^TAG (.*)/)) 305 | self.git_tag_name = self.safe_patch_name($1) 306 | end 307 | @git_author_name, @git_author_email = AUTHOR_MAP[author] 308 | end 309 | 310 | def safe_patch_name(darcs_name) 311 | # See 'man git-check-ref-format' 312 | darcs_name.gsub(/\/+/, '/').gsub(/(?:[\s~\^\\:*\?\[]+|\.{2,}|\#\{|^\.|\.$|^\/|\/$|[^\040-\177])/, '_').gsub(/_+/, '_') 313 | end 314 | 315 | def <=>(other) 316 | self.identifier <=> other.identifier 317 | end 318 | 319 | def git_commit_message 320 | patch_name = ((inverted ? "UNDO: #{name}" : name) unless name =~ /^\[\w+ @ \d+\]/) 321 | OPTIONS[:clean_commit_messages] ? [ patch_name, comment && comment.gsub(/^Ignore-this:.*$/, '')].compact.join("\n") \ 322 | : [ patch_name, comment, "darcs-hash:#{identifier}" ].compact.join("\n\n") 323 | end 324 | 325 | def self.read_from_repo(repo) 326 | REXML::Document.new(output_of("darcs", "changes", "--reverse", 327 | "--repo=#{repo}", "--xml", 328 | "--no-summary")). 329 | get_elements('changelog/patch').map do |p| 330 | DarcsPatch.new(repo, p) 331 | end 332 | end 333 | 334 | # Return committish for corresponding patch in current git repo, or false/nil 335 | def id_in_git_repo 336 | COMMIT_HISTORY.find_git_commit(is_tag, git_tag_name, identifier) 337 | end 338 | 339 | def pull_and_apply 340 | puts "\n" + ("=" * 80) 341 | puts "PATCH : #{name}" ## "1 of 43" 342 | puts "DATE : #{date}" 343 | puts "AUTHOR: #{author} => #{git_author_name} <#{git_author_email}>" 344 | puts "-" * 80 345 | 346 | pull 347 | puts output_of("git", "status", "--short") unless OPTIONS[:quiet] 348 | commit_to_git_repo 349 | end 350 | 351 | private 352 | 353 | def pull 354 | if OPTIONS[:do_checks] and not darcs_reports_clean_repo? 355 | raise "Darcs reports dirty repo before pulling #{identifier}; confused, so aborting" 356 | end 357 | run("darcs", "pull", "--all", "--quiet", 358 | "--match", "hash #{identifier}", 359 | "--set-scripts-executable", "--set-default", source_repo) 360 | if OPTIONS[:do_checks] and not darcs_reports_clean_repo? 361 | puts "Darcs reports dirty directory: assuming conflict that is fixed by a later patch... reverting" 362 | run("darcs revert --all") 363 | run("find . -name '*-darcs-backup[0-9]' -o -name '*.~[0-9]~' -exec rm -rf {} \\;") # darcs2 creates these 364 | unless darcs_reports_clean_repo? 365 | system("darcs whatsnew -sl") 366 | raise "Failed to clean repo, see above" 367 | end 368 | end 369 | end 370 | 371 | def darcs_reports_clean_repo? 372 | `darcs whatsnew -sl | egrep -v '^a (\./)?\.git(/|$)' | egrep -v '^a ./darcs_testing_for_nfs/$'` =~ /^(No changes!)?$/ 373 | end 374 | 375 | def latest_commit_id 376 | git_repo_empty? ? 'NONE' : output_of("git", "rev-list", "-n1", "--no-color", "HEAD").scan(/^([a-z0-9]+)$/).flatten.first 377 | end 378 | 379 | def git_repo_empty? 380 | output_of("git", "branch").strip == '' 381 | end 382 | 383 | def commit_to_git_repo 384 | ENV['GIT_AUTHOR_NAME'] = ENV['GIT_COMMITTER_NAME'] = git_author_name 385 | ENV['GIT_AUTHOR_EMAIL'] = ENV['GIT_COMMITTER_EMAIL'] = git_author_email 386 | ENV['GIT_AUTHOR_DATE'] = ENV['GIT_COMMITTER_DATE'] = date 387 | if is_tag 388 | if git_repo_empty? 389 | STDERR.write("Can't tag an empty git repo: skipping tag '#{git_tag_name}'\n") 390 | else 391 | run("git", "tag", "-a", "-f", "-m", git_commit_message, git_tag_name) 392 | end 393 | else 394 | new_files, changed_files = git_ls_files 395 | if new_files.any? 396 | run(*(["git", "add", "-f"] + new_files)) 397 | end 398 | if changed_files.any? || new_files.any? 399 | output_of("git", "commit", "-a", "-m", git_commit_message) 400 | end 401 | end 402 | # get full id of last commit and associate it with the patch id 403 | COMMIT_HISTORY.record_git_commit(latest_commit_id, identifier) 404 | end 405 | 406 | def darcs_date_to_git_date(utc,local) 407 | # Calculates a git-friendly date (e.g., timezone CET decribed as 408 | # +0100) by using the two date fields that darcs gives us: a list 409 | # of numbers describing the UTC time and a local time formatted in 410 | # a human-readable format. We could parse the local time and 411 | # derive the timezone offset from the timezone name. but timezones 412 | # aren't well-defined, so we ignore the timezone name and instead 413 | # calculate the timezone offset ourselves by calculating the 414 | # difference between local time and UTC time. 415 | 416 | # example: Mon Oct 2 14:23:28 CEST 2006 417 | pat = /^\w\w\w (\w\w\w) ([ 1-9]\d) ([ 0-9]\d)\:(\d\d)\:(\d\d) [\w ]*? (\d\d\d\d)/ 418 | # UTC time may be in the format 20121131154505, or in the regular pattern above 419 | utc_time = if utc =~ /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ 420 | Time.utc($1,$2,$3,$4,$5,$6) 421 | elsif utc =~ pat 422 | Time.utc($6,$1,$2,$3,$4,$5) 423 | else 424 | raise "Wrong darcs date format: #{utc.inspect}" 425 | end 426 | # everything except timezone name is fixed-length, if parsing 427 | # fails we just use UTC 428 | local_time = if pat =~ local 429 | Time.utc($6,$1,$2,$3,$4,$5) 430 | else 431 | utc_time 432 | end 433 | offs = local_time - utc_time # time offset in seconds 434 | t = local_time 435 | # formats the above example as: 2006-10-02 14:23:28 +0200 436 | sprintf("%4d-%02d-%02d %02d:%02d:%02d %s%02d%02d", 437 | t.year, t.month, t.day, 438 | t.hour, t.min, t.sec, 439 | offs < 0 ? "-" : "+", offs.abs/3600, offs.abs.modulo(3600)/60 ) 440 | end 441 | 442 | def git_ls_files 443 | summary = output_of(*["git", "ls-files", "-t", "-o", "-m", "-d", "-z", "-X", ".git/info/exclude"]) 444 | new_files, changed_files = [%w(?), %w(R C)].map do |wanted| 445 | summary.scan(/(.?) (.*?)\0/m).map do |code, name| 446 | name if wanted.include?(code) 447 | end.compact 448 | end 449 | end 450 | end 451 | 452 | def extract_authors(patches) 453 | unique_authors = {} 454 | patches.each do |p| 455 | unique_authors[p.author] = 456 | "#{p.git_author_name}" + (p.git_author_email.nil? ? "" : " <#{p.git_author_email}>") 457 | end 458 | puts "# You can use the following output as a starting point for an author_map" 459 | puts "# Just fill in the proper text after the colon; put email addresses in" 460 | puts "# angle brackets. You can remove any lines that look OK to you." 461 | # TODO: Can we make the output sorted? 462 | puts YAML::dump( unique_authors ) 463 | end 464 | 465 | 466 | # ------------------------------------------------------------------------------- 467 | # Pre-flight checks 468 | # ------------------------------------------------------------------------------- 469 | 470 | DARCS_VERSION = output_of(*%w(darcs -v)).scan(/(\d+)\.(\d+)(?:\.(\d+))?/).flatten.map {|v| v.to_i} 471 | 472 | def darcs2_repo?(repo) 473 | if File.exist?(repo) # Local repo 474 | begin 475 | output_of("darcs", "show", "repo", "--repo=#{repo}") =~ /Format:.*darcs-2/ 476 | rescue # darcs1 does not have a "show" command, so we get an exception 477 | false 478 | end 479 | else 480 | format_file = open(repo.scan(/^(.*?)\/?$/).flatten.first + "/_darcs/format") { |f| f.read } rescue nil 481 | return format_file && (format_file =~ /darcs-2(?:\.|$)/) 482 | end 483 | end 484 | 485 | class Array; include Comparable; end 486 | 487 | unless DARCS_VERSION > [1, 0, 7] 488 | STDERR.write("WARNING: your darcs appears to be old, and may not work with this script\n") 489 | end 490 | 491 | 492 | # ------------------------------------------------------------------------------- 493 | # Initialise the working area 494 | # ------------------------------------------------------------------------------- 495 | ENV['GIT_PAGER'] = ENV['PAGER'] = "cat" # so that pager of git-log doesn't halt conversion 496 | 497 | unless File.directory?("_darcs") 498 | puts "Initialising the working area." 499 | 500 | if Dir.entries(Dir.pwd).delete_if { |e| e == ".git" }.size != 2 501 | raise "Directory not empty. Aborting" 502 | end 503 | if File.directory?(".git") 504 | puts "Detected pre-created git repository. If this is not an empty repo, you may encounter problems." 505 | else 506 | puts "Initializing git repo" 507 | run("git", "init") 508 | end 509 | 510 | darcs_init = %w(darcs init) 511 | if darcs2_repo?(SRCREPO) 512 | darcs_init << "--darcs-2" 513 | elsif DARCS_VERSION >= [2, 0, 0] 514 | puts "Using legacy darcs inventory format to match upstream repo" 515 | if DARCS_VERSION >= [2, 10, 0] 516 | darcs_init << "--darcs-1" 517 | elsif DARCS_VERSION >= [2, 7, 99] 518 | darcs_init << "--hashed" 519 | else 520 | darcs_init << "--old-fashioned-inventory" 521 | end 522 | end 523 | run(*darcs_init) 524 | 525 | FileUtils.mkdir_p(".git/info") 526 | File.open(".git/info/exclude", "a+") { |f| f.write("_darcs\n.DS_Store\n") } 527 | 528 | # Patterns to exclude 529 | git_borings = [] << '(^|/)\.git($|/)' << '(^|/)\.DS_Store$' 530 | existing_borings = [] 531 | 532 | # Check existing global boring patterns 533 | global_darcs_dir = Pathname.new("#{ENV['HOME']}/.darcs") 534 | global_boring_file = global_darcs_dir + 'boring' 535 | if global_boring_file.exist? 536 | existing_borings = File.open(global_boring_file, 'r') {|f| f.readlines}.map {|l| l.chomp } 537 | else 538 | global_darcs_dir.mkdir unless global_darcs_dir.directory? 539 | end 540 | 541 | # Add boring patterns to global boring file 542 | File.open(global_boring_file, 'a') do |f| 543 | (git_borings - existing_borings).each {|b| f.puts b } 544 | end 545 | 546 | # TODO: migrate darcs borings into git excludes? 547 | end 548 | 549 | 550 | COMMIT_HISTORY = CommitHistory.new(GIT_PATCHES) 551 | 552 | 553 | AUTHOR_MAP = if OPTIONS[:author_map] 554 | AuthorMap.load(OPTIONS[:author_map]) 555 | elsif File.exist?(DEFAULT_AUTHOR_MAP_FILE) 556 | AuthorMap.load(DEFAULT_AUTHOR_MAP_FILE) 557 | else 558 | AuthorMap.new 559 | end 560 | AUTHOR_MAP.default_author = OPTIONS[:default_author] 561 | AUTHOR_MAP.default_email = OPTIONS[:default_email] 562 | 563 | 564 | patches = DarcsPatch.read_from_repo(SRCREPO) 565 | if OPTIONS[:list_authors] 566 | extract_authors(patches) 567 | exit(0) 568 | end 569 | 570 | if COMMIT_HISTORY.empty_repo? 571 | patches_available = patches 572 | else 573 | patches_available = patches.find_all { |p| not p.id_in_git_repo } 574 | end 575 | 576 | patches_to_pull = if OPTIONS[:num_patches] 577 | patches_available.first(OPTIONS[:num_patches]) 578 | else 579 | patches_available 580 | end 581 | 582 | original_stdout = $stdout 583 | if OPTIONS[:quiet] 584 | # Capture all writes to stdout. 585 | $stdout = StringIO.new 586 | end 587 | 588 | patches_to_pull.each_with_index { |p, i| 589 | original_stdout.puts "\nImporting patch #{i+1} of #{patches_to_pull.size}:" 590 | p.pull_and_apply 591 | } 592 | 593 | $stdout = original_stdout 594 | 595 | pulled = patches_to_pull.size 596 | if pulled == 0 597 | puts "\nNothing to pull." 598 | else 599 | puts "\nPulled #{pulled} patch#{"es" unless pulled == 1}." 600 | puts "\nDarcs import successful! You may now want to run `git gc' to 601 | optimize space usage of the git repo" 602 | end 603 | 604 | 605 | # ------------------------------------------------------------------------------- 606 | # Post-flight checks 607 | # ------------------------------------------------------------------------------- 608 | 609 | # if we didn't pull all patches, then the consistency check would 610 | # fail, so we simply skip it 611 | if patches_to_pull.size == patches_available.size 612 | if File.exist?(SRCREPO) 613 | puts "Comparing final state with source repo..." 614 | system("diff", "-ur", "-x", "_darcs", "-x", ".git", ".", SRCREPO) 615 | if $? != 0 616 | abort <<-end_msg 617 | !!! There were differences! See diff above for details. 618 | !!! It may be that the source repository was dirty. 619 | !!! Run "cd #{SRCREPO} && darcs whatsnew -sl" to check. 620 | end_msg 621 | else 622 | puts "Contents match." 623 | end 624 | else 625 | puts "Warning: source repo is not local, so we can't compare final git state with latest darcs contents." 626 | end 627 | end 628 | --------------------------------------------------------------------------------