Source

amp / lib / amp / repository / mercurial / repo_format / updater.rb

Full commit
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
#######################################################################
#                  Licensing Information                              #
#                                                                     #
#  The following code is a derivative work of the code from the       #
#  Mercurial project, which is licensed GPLv2. This code therefore    #
#  is also licensed under the terms of the GNU Public License,        #
#  verison 2.                                                         #
#                                                                     #
#  For information on the license of this code when distributed       #
#  with and used in conjunction with the other modules in the         #
#  Amp project, please see the root-level LICENSE file.               #
#                                                                     #
#  © Michael J. Edgar and Ari Brown, 2009-2010                        #
#                                                                     #
#######################################################################

module Amp
  module Repositories
    module Mercurial
      
      ##
      # This module contains all the code that makes a repository able to
      # update its working directory.
      module Updating
        include Amp::Mercurial::RevlogSupport::Node
        
        ##
        # Updates the repository to the given node. One of the major operations on a repository.
        # This means replacing the working directory with the contents of the given node.
        #
        # @todo add lock
        # @param [String, Integer] node the revision to which we are updating the repository. Can
        #   be either nil, a node ID, or an integer. If it is nil, it will update
        #   to the latest revision.
        # @param [Boolean] branch_merge whether to merge between branches
        # @param [Boolean] force whether to force branch merging or file overwriting
        # @param [Proc, #call] filter a function to filter file lists (dirstate not updated if this
        #   is passed)
        # @return [Array<Integer>] a set of statistics about the update. In the form:
        #   [updated, merged, removed, unresolved] where each entry is the # of files in that category.
        def update(node=nil, branch_merge=false, force=false, filter=nil)
          updater = Updater.new(self, :node => node, 
                                      :branch_merge => branch_merge, 
                                      :force => force, 
                                      :filter => filter)
          updater.update
        end
        
        ##
        # Switch to a different branch
        def switch(brnch, opts={})
          opts[:force] ? clean(brnch) : update(brnch)
        end
        
        ##
        # Merge two heads
        def merge(node, force=false)
          update node, true, force, false
        end
        
        ##
        # Updates the repository to the given node, clobbering (removing) changes
        # along the way. This has the effect of turning the working directory into
        # a pristine copy of the requested changeset. Really just a nice way of
        # skipping some arguments for the caller.
        #
        # @param [String] node the requested node
        def clean(node)
          update node, false, true, nil
        end
        
        ##
        # An Action that an updater takes, such as merging, getting a remote file,
        # removing a file, etc.
        module Action
          def self.for(*args)
            action = args.shift.to_s
            klass = Action.module_eval { const_get("#{action[0,1].upcase}#{action[1..-1]}Action") }
            klass.new(*args)
          end
          
          ##
          # This action will replace the requested file's contents
          # in the working directory with the file's data in the "target changeset" -
          # the changeset we're trying to switch to. This will overwrite any existing
          # data in the working directory.
          #
          # @member [String] file the filename to retrieve and restore
          # @member [String] flags the flags to write to the file, such as "x" for executable.
          class GetAction < Struct.new(:file, :flags)
            def apply(repo, stats, updater)
              UI.note("getting #{file}")
              data = updater.target_changeset[file].data
              repo.working_write(file, data, flags)
              stats[:updated] << file
            end
            def record(dirstate, branch_merge)
              branch_merge and dirstate.dirty(file) or dirstate.normal(file)
            end
          end
          
          ##
          # This action removes the given file from the working directory. No
          # ifs, ands, or buts.
          #
          # @member [String] file the file to remove from the working directory
          class RemoveAction < Struct.new(:file)
            def apply(repo, stats, updater)
              UI.note "removing #{file}"
              File.unlink(repo.working_join(file))
              stats[:removed] << file
            end
            def record(dirstate, branch_merge)
              branch_merge and dirstate.remove(file) or dirstate.forget(file)
            end
          end
          
          ##
          # This action adds an existing file to the dirstate so it will be
          # marked as clean by status checks after the update.
          #
          # @member [String] file the file to add to the dirstate
          class AddAction < Struct.new(:file)
            def apply(repo, stats, updater)
            end
            def record(dirstate, branch_merge)
              dirstate.add file unless branch_merge
            end
          end
          
          ##
          # This action removes a file from the dirstate (but does not remove
          # the file!), so it will show up as "unknown" after the update.
          #
          # @member [String] file the file to treat as untracked
          class ForgetAction < Struct.new(:file)
            def apply(repo, stats, updater)
            end
            def record(dirstate, branch_merge)
              dirstate.forget file
            end
          end
          
          ##
          # This action will set the file's executable bit either on or off. This
          # implies that the file does not need to have its data changed, but only
          # the executable bit.
          #
          # @member [String] file the file to modify
          # @member [String] flags the new flags to use
          class ExecAction < Struct.new(:file, :flags)
            def apply(repo, stats, updater)
              FileHelpers.set_executable(repo.working_join(file), flags.include?('x'))
            end
            def record(dirstate, branch_merge)
            end
          end
          
          ##
          # Merges the files from 2 changesets and writes this to a file in the working
          # directory. Since moves and copies could have happened, the files could have
          # a different name in the two revisions, which are the file/remote_file options.
          # This goes through the merge state to handle the merging.
          #
          # @member [String] file the path to the file in the source/local changeset
          # @member [String] remoter_file the path to the file in the target/remote changeset
          # @member [String] file_dest the path to use for the file after the merge
          # @member [String] flags The flags to use after the merge on file_dest
          # @member [Boolean] move Is the file not located at +file+ in the target changeset?
          class MergeAction < Struct.new(:file, :remote_file, :file_dest, :flags, :move)
            def apply(repo, stats, updater)
              result = repo.merge_state.resolve(file_dest, updater.working_changeset, updater.target_changeset)
              
              if    result          then stats[:unresolved] << file
              elsif result.nil?     then stats[:updated] << file
              elsif result == false then stats[:merged] << file
              end

              FileHelpers.set_executable(repo.working_join(file_dest), flags && flags.include?('x'))
              if (file != file_dest && move && File.amp_lexist?(repo.working_join(file)))
                UI.debug("removing #{file}")
                File.unlink(repo.working_join(file))
              end
            end
            def record(dirstate, branch_merge)
              if branch_merge
                dirstate.merge(file_dest)
                if file != remote_file #copy/rename
                  dirstate.remove file if move
                  dirstate.copy(file,  file_dest) if file != file_dest
                  dirstate.copy(remote_file, file_dest) if file == file_dest
                end
              else
                dirstate.maybe_dirty(file_dest)
                dirstate.forget(file) if move
              end
            end
          end
          
          ##
          # Just warns when a file goes from an ancestor node, with one name, to
          # two different names in its children. That's a bad thing.
          class Divergent_renameAction < Struct.new(:file, :newfiles)
            def apply(repo, stats, updater)
              UI.warn("detected divergent renames of #{file} to:")
              newfiles.each {|fn| UI.warn fn }
            end
            def record(dirstate, branch_merge)
            end
          end
          
          ##
          # Handles a moved/missing directory when merging a file.
          #
          # This is similar to a merge, except we *don't know one of the filenames*, because
          # a directory got renamed somewhere. So either :file or :remote_file is going to
          # be nil.
          #
          # @member [String, nil] file the local name of the file, or nil if it couldn't be found
          # @member [String, nil] remote_file the name of the file in the target changeset, or
          #   nil if it couldn't be found.
          # @member [String] file_dest The name to use for the file after the merge
          # @member [String] flags the flags to use for the file after the merge
          class DirectoryAction < Struct.new(:file, :remote_file, :file_dest, :flags)
            def apply(repo, stats, updater)
              if file && file.any?
                UI.note("moving #{file} to #{file_dest}")
                File.move(file, file_dest)
              end
              if remote_file && remote_file.any?
                UI.note("getting #{remote_file} to #{file_dest}")
                data = updater.target_changeset[remote_file].data
                repo.working_write(file_dest, data, flags)
              end
              stats[:updated] << file
            end
            def record(dirstate, branch_merge)
              return unless remote_file || dirstate.include?(file)
              if branch_merge
                dirstate.add file_dest
                if file && file.any?
                  dirstate.remove file
                  dirstate.copy file, file_dest
                end
                dirstate.copy remote_file, file_dest if remote_file && remote_file.any?
              else
                dirstate.normal file_dest
                dirstate.forget file if file && file.any?
              end
            end
          end
        end
        
        ##
        # Class responsible for logic relating to updating the repository.
        #
        # Encapsulates one update operation using instance variables. This saves
        # the trouble of a purely functional approach, where we must pass around
        # a hash of values through each function, which just isn't necessary. Though
        # it is cute.
        class Updater
          
          ##
          # the default options for running an update.
          DEFAULT_OPTIONS = {:node => nil, :branch_merge => false, :force => false, :filter => nil}
          
          attr_reader :node, :branch_merge, :force, :filter, :repo
          attr_reader :working_changeset, :target_changeset
          attr_accessor :remote, :local, :ancestor
          
          ##
          # Creates an updater object, which will guide the repository through
          # an update of its working directory.
          #
          # @param [LocalRepository] repo the repository to work on
          # @param [Hash] opts the options for this update operation
          # @option opts [String, Integer] :node (nil) the node of the repository
          #    to update to. Will be passed into the repository's lookup method,
          #    so this can be a string or integer or what have you.
          # @option opts [Boolean] :branch_merge (false) is this a branch merge?
          #    in other words, if we run into conflicts, should that be expected?
          # @option opts [Boolean] :force (false) do we force the update, even
          #    if something unexpected happens?
          # @option opts [Proc, #call] :filter (nil) A proc that will help us
          #    filter out files. If this is passed, the dirstate isn't updated.
          def initialize(repo, opts = {})
            @repo = repo
            opts = DEFAULT_OPTIONS.merge(opts)
            @node, @branch_merge = opts[:node] , opts[:branch_merge]
            @force, @filter      = opts[:force], opts[:filter]
            
            initialize_ivars
          end
          
          ##
          # Sets up some useful ivars that we will need for a shit ton of processing
          # as we prepare for this update operation
          def initialize_ivars
            @local = @working_changeset = @repo[nil]
            
            # tip of current branch
            @node ||= @repo.branch_tags[@working_changeset.branch]
            @node = @repo.lookup("tip") if @node.nil? && @working_changeset.branch == "default"
            @target_changeset = @repo[@node]
            
            @remote = @repo[@node]
            @local_parent = @working_changeset.parents.first
            @ancestor = @local_parent.ancestor(@remote)
            @overwrite = false
          end
          
          ##
          # Can we overwrite files?
          #
          # @return [Boolean] whether or not overwriting files is OK
          def overwrite?
            puts(@overwrite || (force && !branch_merge))
            @overwrite || (force && !branch_merge)
          end
          
          ##
          # Is this a valid fast-forward merge?
          #
          # @return [Boolean] is it a fast-forward merge/
          def fast_forward?
            branch_merge && ancestor != remote && 
                            ancestor == @local_parent && 
                            @local_parent.branch != remote.branch
          end
          
          ##
          # Is this a backwards, linear update?
          #
          # @return [Boolean] is it a fast-forward merge/
          def backwards?
            remote == ancestor
          end
          
          ##
          # Runs the update specified by this Updater object's properties.
          #
          # @return [Array<Integer>] a set of statistics about the update. In the form:
          #   [updated, merged, removed, unresolved] where each entry is the # of files in that category.
          def update
            verify_no_uncommitted_merge
            verify_valid_update
            
            check_unknown if force
            check_collision if false # case-insensitive file-system? seriously? (uh... mac os x ring a bell?)
            
            @actions = []
            
            forget_removed
            manifest_merge
            
            stats = apply_updates
            record_updates
            stats
          end
          
          ##
          # Adds an action to the action list (stuff to do).
          #
          # @param [String] file the filename to use
          # @param [Symbol] act the action to perform (such as :merge, :get)
          # @param [Array] args the extra arguments to the action.
          def act(act, file, *args)
            @actions << Action.for(act, file, *args)
          end
          
          ##
          # Adds an "add" action with the given file
          #
          # @see act
          # @param [String] file the file to add
          def add(file); @actions << Action::AddAction.new(file); end
          
          ##
          # Adds a "remove" action with the given file
          #
          # @see act
          # @param [String] file the file to remove
          def remove(file); @actions << Action::RemoveAction.new(file); end
          
          ##
          # Adds a "forget" action with the given file
          #
          # @see act
          # @param [String] file the file to forget
          def forget(file); @actions << Action::ForgetAction.new(file); end
          
          ##
          # Adds a "get" action for the given file and flags.
          # This action replaces the local file with the remote file 
          # with the given name and sets its flags to the specified flag.
          def get(file, flags); @actions << Action::GetAction.new(file, flags); end
          
          ##
          # Adds a "set flags" action with the given file and flags.
          # Used when the file needs only to have its flags changed to match the
          # target action
          #
          # @see act
          # @param [String] file the file to modify
          # @param [String] flags the flags to set
          def set_flags(file, flags); @actions << Action::ExecAction.new(file, flags); end
          
          ##
          # Adds a "merge" action with all the necessary information to merge
          # the two files.
          #
          # We need the working-directory filename, the target changeset filename, the
          # final name to use (we have to pick one, if they're different). We also need
          # the flags to use at the end, and we should know if a move is going to happen.
          #
          # @param [String] file the filename in the working changeset
          # @param [String] remote_file the filename in the target changeset
          # @param [String] file_dest the filename to use after merging
          # @param [String] flags the flags to assign the file when we finish merging
          # @param [Boolean] move should we move the file?
          def merge(file, remote_file, file_dest, flags, move) 
            @actions << Action::MergeAction.new(file, remote_file, file_dest, flags, move)
          end
          
          ##
          # Adds a "directory rename" action with all the necessary information to merge
          # the two files.
          #
          # We need the working-directory filename, the target changeset filename, the
          # final name to use (we have to pick one, if they're different. We also need
          # the flags to use at the end.
          #
          # This is similar to a merge, except we *don't know one of the filenames*, because
          # a directory got renamed somewhere. So either :file or :remote_file is going to
          # be nil.
          #
          # @param [String] file the filename in the working changeset
          # @param [String] remote_file the filename in the target changeset
          # @param [String] file_dest the filename to use after merging
          # @param [String] flags the flags to assign the file when we finish merging
          def directory(file, remote_file, file_dest, flags)
            @actions << Action::DirectoryAction.new(file, remote_file, file_dest, flags)
          end
          
          ##
          # Adds a "divergent rename" action to the list. This action points out
          # that the same file has been renamed to a number of different possible names.
          # This just warns the user about it - there's no way we can reliably resolve
          # this for them.
          #
          # @param [String] file the original (working directory) filename
          # @param [Array<String>] other_files the list of the names this file could
          #   actually be
          def divergent_rename(file, other_files)
            @actions << Divergent_renameAction.new(file, other_files)
          end
          
          ##
          # Raises an abort if the working changeset is actually a merge (in which case
          # we have to commit first)
          def verify_no_uncommitted_merge
            if !overwrite? && @working_changeset.parents.size > 1
              raise abort("outstanding uncommitted merges")
            end
          end
          
          ##
          # Verifies that this update is valid. This is based on
          # the type of the udpate - if it's a branch merge, we have to make
          # sure it's a valid merge. If it's a non-destructive update, we
          # have to make sure we're not doing something destructive!
          def verify_valid_update
            if branch_merge
              verify_valid_branch_merge
            elsif !overwrite?
              verify_non_destructive
            end
          end
          
          ##
          # Verifies that the update is a valid branch merge. Just raises aborts
          # when the user does something he's not supposed to.
          def verify_valid_branch_merge
            # trying to merge backwards with a direct ancestor of the current directory.
            # that's crazy.
            if ancestor == remote
              raise abort("can't merge with ancestor")
            elsif ancestor == @local_parent
              # If we're at the branch point, without a difference in branch names, just do an update. 
              # Kind of the opposite of the last case, only isntead of trying to merge directly backward,
              # we're trying to merge directly forward. That's wrong.
              if @local_parent.branch == remote.branch
                raise abort("nothing to merge (use 'amp update' or check"+
                                     " 'amp heads')")
              end
            end
            # Can't merge when you have a dirty working directory. We don't want to lose
            # those changes!
            if !force && (working_changeset.changed_files.any? || working_changeset.deleted.any?)
              raise abort("oustanding uncommitted changes")
            end
          end
          
          ##
          # Verifies that the update is non-destructive. The user is simply trying
          # to load a different revision into their working directory. No harm, no
          # foul.
          def verify_non_destructive
            # Obviously non-destructive because we have a linear path.
            if ancestor == @local_parent || ancestor == remote
              p 5
              @overwrite = true
              return
            end
            
            # At this point, they obviously are crossing a branch.
            if @local_parent.branch == remote.branch
              # Here's how this work: if you want to cross a *revision history branch* (not
              # a named branch), you have to do a branch merge. So that's not allowed.
              #
              # If dirty, print a special message about your changes
              if working_changeset.changed_files.any? || working_changeset.deleted.any?
                raise abort("crosses branches (use 'amp merge' or "+
                                     "'amp update -C' to discard changes)")
              end
              # Otherwise, just let them know they can't cross branches.
              raise abort("crosses branches (use 'amp merge' or 'amp update -C')")
            elsif working_changeset.changed_files.any? || working_changeset.deleted.any?
              # They're crossing to a named branch, but have a dirty working dir. not allowed.
              raise abort("crosses named branches (use 'amp update -C'"+
                                   " to discard changes)")
            else
              # They just want to switch to a named branch. That's ok, as long as they
              # have no uncommitted changes.
              @overwrite = true
            end
          end
          
          
          ##
          # This method will make sure that there are no differences between
          # untracked files in the working directory, and tracked files in
          # the new changeset. 
          #
          # @raise [AbortError] if an untracked file in the working directory is different from
          #   a tracked file in the target changeset, this abort error will be raised.
          def check_unknown
            working_changeset.unknown.each do |file|
              if target_changeset.all_files.include?(file) && target_changeset[file].cmp(working_changeset[file].data())
                raise abort("Untracked file in the working directory differs from "+
                                     "a tracked file in the requested revision: #{file} #{target_changeset[file]}")
              end
            end
          end
          
          ##
          # This method will check if the target changeset will cause name collisions
          # when filenames are changed to all lower-case. This is important because
          # in the store, the file-logs are all changed to lower-case.
          #
          # @raise [AbortError] If two files have the same lower-case name, in 1 changeset,
          #   this error will be thrown.
          def check_collision
            target_changeset.inject({}) do |folded_names, file|
              folded = file.downcase
              if folded_names[folded]
                raise abort("Case-folding name collision between #{folded_names[folded]} and #{file}.")
              end
              folded_names[folded] = file
            end
          end
          
          ##
          # Forget removed files (docs ripped from mercurial)
          # 
          # If we're jumping between revisions (as opposed to merging), and if
          # neither the working directory nor the target rev has the file,
          # then we need to remove it from the dirstate, to prevent the
          # dirstate from listing the file when it is no longer in the
          # manifest.
          # 
          # If we're merging, and the other revision has removed a file
          # that is not present in the working directory, we need to mark it
          # as removed.
          #
          # Adds actions to our global list of actions.
          def forget_removed
            action = branch_merge ? :remove : :forget
            working_changeset.deleted.each do |file|
              act action, file unless target_changeset[file]
            end

            unless branch_merge
              working_changeset.removed.each do |file|
                forget file unless target_changeset[file]
              end
            end
          end
          
          ##
          # Should the given file be filtered out by the updater?
          #
          # @param [String] file the filename to run through the filter (if any filter)
          # @return [Boolean] should the file be filtered?
          def should_filter?(file)
            filter && !filter.call(file)
          end
          
          def local_manifest; local.manifest_entry; end
          def remote_manifest; remote.manifest_entry; end
          def ancestor_manifest; ancestor.manifest_entry; end
          
          ##
          # Merge the local working changeset (local), and the target changeset (remote),
          # using the common ancestor (ancestor). Generates a merge action list to update
          # the manifest.
          #
          # @return [[String, Symbol]] A list of actions that should be taken to complete
          #   a successful transition from local to remote.
          def manifest_merge
            UI::status("resolving manifests")
            UI::debug(" overwrite #{overwrite?} partial #{filter}")
            UI::debug(" ancestor #{ancestor} local #{local} remote #{remote}")
            
            copy = calculate_copies
            copied_files = Hash.with_keys(copy.values)
            
            # Compare manifests
            working_changeset.each do |file, node|
              update_local_file file, node, copy, copied_files
            end
            
            remote.each do |file, node|
              update_remote_file file, node, copy, copied_files
            end
          end
          
          ##
          # Create an action to perform to update a file that exists in our
          # local working changeset. This will require a bit of logic, because
          # there's all kinds of specific cases we have to narrow through.
          #
          # @param [String] file the filename we have in our working directory
          # @param [String] node an identifying node ID for the file's revision
          # @param [Hash] copy the map of copies between the two changesets
          # @param [Hash] copied_files a lookup hash for checking if a file has
          #   been involved in a copy
          def update_local_file(file, node, copy, copied_files)
            return if should_filter? file

            # Is the file also in the target changeset?
            if remote.include? file
              update_common_file file, node
            elsif copied_files[file]
              return
            elsif copy[file]
              update_locally_copied file, copy[file]
            elsif ancestor_manifest[file]
              update_remotely_deleted file, node
            else
              # Are we overwriting with empty data, or going backwards to when the
              # file didn't exist?
              if (overwrite? && node[20..-1] == "u") || (backwards? && node.size <= 20)
                remove file
              end
            end
          end
          
          ##
          # Create an action to perform to update a file that exists in the remote
          # changeset. This will involve checking to see if it also exists in the
          # local changeset, because if it covers a case we've already seen, we
          # shouldn't do anything. However, by using this method, we can find
          # files that have shown up in the target branch but haven't existed
          # in the working branch.
          #
          # @param [String] file the file to inspect
          # @param [String] node the node of the file
          # @param [Hash] copy the copy map involved in the changesets
          # @param [Hash] copied_files a lookup hash for checking if a file has
          #   been involved in a copy
          def update_remote_file(file, node, copy, copied_files)
            return if should_filter?(file) || 
                      local_manifest[file] ||
                      copied_files[file]
            
            if copy[file]
              # If it's been copied, then we might need to do some work.
              update_copied_remote_file file, copy
            elsif ancestor.include? file
              # If the ancestor has the file, and the target has the file, and we don't,
              # then we'll probably have to do some merging to fix that up.
              update_remotely_modified_file file, node
            else
              # Just get the god damned file
              get file, remote.flags(file)
            end
          end
          
          ##
          # Create an action to update a file in the target changeset that has been
          # copied locally. This will create some interesting scenarios.
          #
          # @param [String] file the name of the file in the target changeset
          # @param [Hash] copy the copy map which organizes copies within changesets
          def update_copied_remote_file(file, copy)
            # the remote has a file that has been copied or moved from copy[file] to file.
            # file is also destination.
            src = copy[file]
            
            # If the user doen't even have the source, then we apparently renamed a directory.
            if !(working_changeset.include?(file2))
              directory nil, file, src, remote.flags(file)
            elsif remote.include? file2
              # If the remote also has the source, then it was copied, not moved
              merge src, file, file, flag_merge(src, file, src), false
            else
              # If the source is gone, it was moved. Hence that little "true" at the end there.
              merge src, file, file, flag_merge(src, file, src), true
            end
          end
          
          def update_remotely_modified_file(file, node)
            if overwrite? || backwards?
              get file, remote.flags(file)
            elsif node != ancestor.file_node(file)
              if UI.ask("remote changed #{file} which local deleted\n" +
                        "use (c)hanged version or leave (d)eleted?") == "c"
                get file, remote.flags(file)
              end
            end
          end
          
          
          ##
          # Create an action to perform on a file that exists in both the working
          # changeset, and the remote changeset. Will only create an action if
          # we need to do something to modify the file to reach the remote state.
          #
          # @param [String] file the filename that is in both the local and remote
          #   changesets
          # @param [String] node the file node ID of the file in the local changeset
          def update_common_file(file, node)
            rflags = (overwrite? || backwards?) ? remote.flags(file) : flag_merge(file,nil,nil)
            # Are files different?
            if node != remote.file_node(file)

              anc_node = ancestor.file_node(file) || NULL_ID
              
              # are we allowed to just overwrite?
              # or are we going back in time to clean up?
              # or is the remote newer along a linear update?
              if overwrite? || (backwards? && (node[20..-1].empty? || !remote[file].cmp(local[file].data))) ||
                               (node == anc_node && remote_manifest[file] != anc_node)
                # replace the local file with the remote file
                puts "diffget #{file}"
                get file, rflags
                return
              elsif node != anc_node && remote_manifest[file] != anc_node
                # are both nodes different from the ancestor?
                puts "diffmerge #{file}"
                merge file, file, file, rflags, false
                return
              end
            end
            if local_manifest.flags[file].to_s != rflags
              # Are the files the same, but have different flags?
              set_flags file, rflags
            end
          end
          
          ##
          # Create an action that will update a file that is not in the target
          # changeset, and has been copied locally. The idea is that if we've copied
          # this file, maybe it's in the other changeset under its old name.
          # We check that, and can create a merge if so. Otherwise, we cry deeply.
          #
          # @param [String] file the name of the file being inspected
          # @param [String] renamed_file the old name of the file
          def update_locally_copied(file, renamed_file)
            if !remote_manifest[renamed_file] 
            # directory rename (I don't know what's going on here)
            then directory file, nil, renamed_file, working_changeset.flags[file]
            # We found the old name of the file in the remote manifest.
            else merge file, renamed_file, file, flag_merge[file, renamed_file, renamed_file], false
            end
          end
          
          ##
          # Locally, we have the file. The ancestor has the file. That bastard
          # remote changeset deleted our file somewhere. What do we do?
          #
          # Well, if we've changed it since the ancestor (i.e., we've been
          # using the file actively), and we aren't allowed to overwrite files, 
          # then we should probably ask. Because that remote changeset didn't 
          # want it, but we clearly do. So ask the user.
          #
          # Otherwise, just remove it.
          #
          # @param [String] file the file in question
          # @param [String] node the file node ID of the file in the local changeset
          def update_remotely_deleted(file, node)
            # 
            if node != ancestor.file_node(file) && !overwrite?
              if UI.ask("local changed #{file} which remote deleted\n" +
                        "use (c)hanged version or (d)elete?") == "d"
              then remove file
              else add file
              end
            else
              remove file
            end
          end
          
          ##
          # Determines which files have been copied, and marks divergent renames
          #
          # @return [Array<String>] a hash mapping copied files to their new name
          def calculate_copies
            # no copies if we don't have an ancestor.
            # no copies if we're going backwards.
            # no copies if we're overwriting.
            return {} unless ancestor && !(backwards? || overwrite?)
            # no copies if the user says not to follow them.
            return {} unless @repo.config["merge", "followcopies", Boolean, true]

            
            dirs = @repo.config["merge", "followdirs", Boolean, false]
            # calculate dem hoes!
            copy, diverge = Amp::Graphs::Mercurial::CopyCalculator.find_copies(self, local, remote, ancestor, dirs)
            
            # act upon each divergent rename (one branch renames to one name,
            # the other branch renames to a different name)
            diverge.each {|of, fl| divergent_rename of, fl }
            
            copy
          end
          
          ##
          # Figure out what the new flags of the file should be. We need to know
          # the name of the file in all 3 important changesets, since there could
          # be moves or copies.
          #
          # @param [String] file_local the name of the file in the local changeset
          # @param [String] file_remote the name of the file in the remote changeset
          # @param [String] file_ancestor the name of the file in the ancestor changeset
          # @return [String] the flags to use for the merged file
          def flag_merge(file_local, file_remote, file_ancestor)
            file_remote = file_ancestor = file_local unless file_remote
            
            a = ancestor.flags file_ancestor
            m = working_changeset.flags file_local
            n = remote.flags file_remote
            
            # flags are identical, so no merging needed
            return m if m == n 
            
            # m and n conflict. How do we pick which one to use?
            if m.any? && n.any?
              # m and n are both flags (not empty).
              
              # if there was no ancestor flag, there's no way to guess. As the user.
              if a.empty?
                r = UI.ask("conflicting flags for #{file_local} (n)one, e(x)ec, or "+
                              "sym(l)ink?")
                return (r != "n") ? r : ''
              end
              # There is an ancestor flag, so we return whichever one differs from the
              # ancestor.
              return m == a ? n : m
            end
            
            # m or n might be set, but not both. Choose one that differs from ancestor.s
            return m if m.any? && m != a # changed from a to m
            return n if n.any? && n != a # changed from a to n
            return '' #no more flag
          end
          
          ##
          # Compare two actions in the update action list
          #
          # @param [Action] action1 the first action
          def action_cmp(action1, action2)
            return action1.to_a <=> action2.to_a if action1.is_a?(action2.class)
            return -1 if action1 === Action::RemoveAction
            return 1  if action2 === Action::RemoveAction
            return action1.to_a <=> action2.to_a
          end
          
          ##
          # Apply the merge action list to the working directory, in order to migrate from
          # working_changeset to target_changeset.
          #
          # @todo add path auditor
          # @param [Array<Array>] actions list of actions to take to migrate from {working_changeset} to
          #   {target_changeset}.
          # @param [WorkingDirectoryChangeset] working_changeset the current changeset in the repository
          # @param [Changeset] target_changeset the changeset we are updating the working directory to.
          # @return [Hash] Statistics about the update. Keys are:
          #   :updated => files that were changed
          #   :merged  => files that were merged
          #   :removed => files that were removed
          #   :unresolved => files that had conflicts when merging that we couldn't fix
          def apply_updates
            results = results_hash
            @repo.merge_state.reset(@local_parent.node)
            @actions.sort! {|a1, a2| action_cmp a1, a2 }

            # If we're moving any files, we can remove renamed ones now
            remove_moved_files

            # TODO: add path auditor
            @actions.each do |action|
              next if action.file && action.file[0,1] == "/"
              action.apply(@repo, results, self)
            end
            
            results
          end
          
          ##
          # Returns a statistics hash: it has the keys necessary for reporting
          # the results of an update/merge. It also has a #success? method on it.
          #
          # @return [Hash] a hash prepared for reporting update/merge statistics
          def results_hash
            hash = {:updated    => [],
                    :merged     => [],
                    :removed    => [],
                    :unresolved => []}

            class << hash
              def success?; self[:unresolved].empty?; end
            end
            hash
          end
          
          ##
          # Removes all moved files in an update/merge. What happens is this:
          # if we have file A, which has been moved to the file B in our target
          # changeset, we're gonna have A lying around. We have to get rid of A.
          # That's what this method does: it finds those left over files, and
          # gets rid of them before we start doing any updates.
          def remove_moved_files
            scan_for_merges.each do |file|
              if File.amp_lexist?(@repo.working_join(file))
                UI.debug("removing #{file}")
                File.unlink(@repo.working_join(file))
              end
            end
          end
          
          ##
          # Add merges in the action list to the merge state. Also, return any
          # merge-moves, so we can process them.
          #
          # @return [Array<String>] a list of files that were both merged and moved,
          #   so we can unlink their original location
          def scan_for_merges
            moves = []
            # prescan for merges in the list of actions.
            @actions.select {|act| act.is_a? Action::MergeAction}.each do |a|
              # destructure the list
              file, remote_file, filename_dest, flags, move = a.file, a.remote_file, a.file_dest, a.flags, a.move
              UI.debug("preserving #{file} for resolve of #{filename_dest}")
              # look up our changeset for the merge state entry
              vf_local = working_changeset[file] 
              vf_other = target_changeset[remote_file]
              vf_base  = vf_local.ancestor(vf_other) || @repo.versioned_file(file, :file_id => Updating::NULL_REV)
              # track this merge!
              @repo.merge_state.add(vf_local, vf_other, vf_base, filename_dest, flags) 

              moves << file if file != filename_dest && move
            end
            moves
          end
          
          ##
          # Record the actions that this updating process has built up and
          # have been applied already.
          def record_updates
            unless filter
              dirstate = @repo.dirstate
              @actions.each do |action|
                action.record(dirstate, action)
              end
              useful_parent_1 = branch_merge ? @local_parent.node : @remote.node
              useful_parent_2 = branch_merge ? @remote.node : Updating::NULL_ID
              
              dirstate.parents = [useful_parent_1, useful_parent_2]
              if !branch_merge && !fast_forward?
                dirstate.branch = @target_changeset.branch
              end

              @repo.run_hook :update, :parent1 => useful_parent_1, :parent2 => useful_parent_2 #, :error => stats[3]
              dirstate.write
            end
          end
        end # class Updater
      end # module Updating
    end # module Mercurial
  end # module Repositories
end # module Amp