920b23e6cb998d130be8d6e246a46c057dfcc317
   1#!/usr/bin/perl -w
   2
   3# This tool is copyright (c) 2005, Matthias Urlichs.
   4# It is released under the Gnu Public License, version 2.
   5#
   6# The basic idea is to pull and analyze SVN changes.
   7#
   8# Checking out the files is done by a single long-running CVS connection
   9# / server process.
  10#
  11# The head revision is on branch "origin" by default.
  12# You can change that with the '-o' option.
  13
  14require v5.8.0; # for shell-safe open("-|",LIST)
  15use strict;
  16use warnings;
  17use Getopt::Std;
  18use File::Spec;
  19use File::Temp qw(tempfile);
  20use File::Path qw(mkpath);
  21use File::Basename qw(basename dirname);
  22use Time::Local;
  23use IO::Pipe;
  24use POSIX qw(strftime dup2);
  25use IPC::Open2;
  26use SVN::Core;
  27use SVN::Ra;
  28
  29$SIG{'PIPE'}="IGNORE";
  30$ENV{'TZ'}="UTC";
  31
  32our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T,$opt_b);
  33
  34sub usage() {
  35        print STDERR <<END;
  36Usage: ${\basename $0}     # fetch/update GIT from CVS
  37       [-o branch-for-HEAD] [-h] [-v]
  38       [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
  39       [-i] [-u] [-s subst] [-m] [-M regex] [SVN_URL]
  40END
  41        exit(1);
  42}
  43
  44getopts("b:C:hivmM:o:t:T:u") or usage();
  45usage if $opt_h;
  46
  47my $tag_name = $opt_t || "tags";
  48my $trunk_name = $opt_T || "trunk";
  49my $branch_name = $opt_b || "branches";
  50
  51@ARGV <= 1 or usage();
  52
  53$opt_o ||= "origin";
  54my $git_tree = $opt_C;
  55$git_tree ||= ".";
  56
  57my $cvs_tree;
  58if ($#ARGV == 0) {
  59        $cvs_tree = $ARGV[0];
  60} elsif (-f 'CVS/Repository') {
  61        open my $f, '<', 'CVS/Repository' or 
  62            die 'Failed to open CVS/Repository';
  63        $cvs_tree = <$f>;
  64        chomp $cvs_tree;
  65        close $f;
  66} else {
  67        usage();
  68}
  69
  70our @mergerx = ();
  71if ($opt_m) {
  72        @mergerx = ( qr/\W(?:from|of|merge|merging|merged) (\w+)/i );
  73}
  74if ($opt_M) {
  75        push (@mergerx, qr/$opt_M/);
  76}
  77
  78select(STDERR); $|=1; select(STDOUT);
  79
  80
  81package SVNconn;
  82# Basic SVN connection.
  83# We're only interested in connecting and downloading, so ...
  84
  85use File::Spec;
  86use File::Temp qw(tempfile);
  87use POSIX qw(strftime dup2);
  88
  89sub new {
  90        my($what,$repo) = @_;
  91        $what=ref($what) if ref($what);
  92
  93        my $self = {};
  94        $self->{'buffer'} = "";
  95        bless($self,$what);
  96
  97        $repo =~ s#/+$##;
  98        $self->{'fullrep'} = $repo;
  99        $self->conn();
 100
 101        $self->{'lines'} = undef;
 102
 103        return $self;
 104}
 105
 106sub conn {
 107        my $self = shift;
 108        my $repo = $self->{'fullrep'};
 109        my $s = SVN::Ra->new($repo);
 110
 111        die "SVN connection to $repo: $!\n" unless defined $s;
 112        $self->{'svn'} = $s;
 113        $self->{'repo'} = $repo;
 114        $self->{'maxrev'} = $s->get_latest_revnum();
 115}
 116
 117sub file {
 118        my($self,$path,$rev) = @_;
 119        my $res;
 120
 121        my ($fh, $name) = tempfile('gitsvn.XXXXXX', 
 122                    DIR => File::Spec->tmpdir(), UNLINK => 1);
 123
 124        print "... $rev $path ...\n" if $opt_v;
 125        eval { $self->{'svn'}->get_file($path,$rev,$fh); };
 126        if (defined $@ and $@ !~ /Attempted to get checksum/) {
 127            # retry
 128            $self->conn();
 129                eval { $self->{'svn'}->get_file($path,$rev,$fh); };
 130        };
 131        return () if defined $@ and $@ !~ /Attempted to get checksum/;
 132        die $@ if $@;
 133        close ($fh);
 134
 135        return ($name, $res);
 136}
 137
 138
 139package main;
 140
 141my $svn = SVNconn->new($cvs_tree);
 142
 143
 144sub pdate($) {
 145        my($d) = @_;
 146        $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)#
 147                or die "Unparseable date: $d\n";
 148        my $y=$1; $y-=1900 if $y>1900;
 149        return timegm($6||0,$5,$4,$3,$2-1,$y);
 150}
 151
 152sub pmode($) {
 153        my($mode) = @_;
 154        my $m = 0;
 155        my $mm = 0;
 156        my $um = 0;
 157        for my $x(split(//,$mode)) {
 158                if($x eq ",") {
 159                        $m |= $mm&$um;
 160                        $mm = 0;
 161                        $um = 0;
 162                } elsif($x eq "u") { $um |= 0700;
 163                } elsif($x eq "g") { $um |= 0070;
 164                } elsif($x eq "o") { $um |= 0007;
 165                } elsif($x eq "r") { $mm |= 0444;
 166                } elsif($x eq "w") { $mm |= 0222;
 167                } elsif($x eq "x") { $mm |= 0111;
 168                } elsif($x eq "=") { # do nothing
 169                } else { die "Unknown mode: $mode\n";
 170                }
 171        }
 172        $m |= $mm&$um;
 173        return $m;
 174}
 175
 176sub getwd() {
 177        my $pwd = `pwd`;
 178        chomp $pwd;
 179        return $pwd;
 180}
 181
 182
 183sub get_headref($$) {
 184    my $name    = shift;
 185    my $git_dir = shift; 
 186    my $sha;
 187    
 188    if (open(C,"$git_dir/refs/heads/$name")) {
 189        chomp($sha = <C>);
 190        close(C);
 191        length($sha) == 40
 192            or die "Cannot get head id for $name ($sha): $!\n";
 193    }
 194    return $sha;
 195}
 196
 197
 198-d $git_tree
 199        or mkdir($git_tree,0777)
 200        or die "Could not create $git_tree: $!";
 201chdir($git_tree);
 202
 203my $orig_branch = "";
 204my $forward_master = 0;
 205my %branches;
 206
 207my $git_dir = $ENV{"GIT_DIR"} || ".git";
 208$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#;
 209$ENV{"GIT_DIR"} = $git_dir;
 210my $orig_git_index;
 211$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
 212my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx',
 213                                    DIR => File::Spec->tmpdir());
 214close ($git_ih);
 215$ENV{GIT_INDEX_FILE} = $git_index;
 216my $maxnum = 0;
 217my $last_rev = "";
 218my $last_branch;
 219my $current_rev = 0;
 220unless(-d $git_dir) {
 221        system("git-init-db");
 222        die "Cannot init the GIT db at $git_tree: $?\n" if $?;
 223        system("git-read-tree");
 224        die "Cannot init an empty tree: $?\n" if $?;
 225
 226        $last_branch = $opt_o;
 227        $orig_branch = "";
 228} else {
 229        -f "$git_dir/refs/heads/$opt_o"
 230                or die "Branch '$opt_o' does not exist.\n".
 231                       "Either use the correct '-o branch' option,\n".
 232                       "or import to a new repository.\n";
 233
 234        -f "$git_dir/svn2git"
 235                or die "'$git_dir/svn2git' does not exist.\n".
 236                       "You need that file for incremental imports.\n";
 237        $last_branch = basename(readlink("$git_dir/HEAD"));
 238        unless($last_branch) {
 239                warn "Cannot read the last branch name: $! -- assuming 'master'\n";
 240                $last_branch = "master";
 241        }
 242        $orig_branch = $last_branch;
 243        $last_rev = get_headref($orig_branch, $git_dir);
 244        if (-f "$git_dir/SVN2GIT_HEAD") {
 245                die <<EOM;
 246SVN2GIT_HEAD exists.
 247Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD.
 248You may need to run
 249
 250    git-read-tree -m -u SVN2GIT_HEAD HEAD
 251EOM
 252        }
 253        system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD");
 254
 255        $forward_master =
 256            $opt_o ne 'master' && -f "$git_dir/refs/heads/master" &&
 257            system('cmp', '-s', "$git_dir/refs/heads/master", 
 258                                "$git_dir/refs/heads/$opt_o") == 0;
 259
 260        # populate index
 261        system('git-read-tree', $last_rev);
 262        die "read-tree failed: $?\n" if $?;
 263
 264        # Get the last import timestamps
 265        open my $B,"<", "$git_dir/svn2git";
 266        while(<$B>) {
 267                chomp;
 268                my($num,$branch,$ref) = split;
 269                $branches{$branch}{$num} = $ref;
 270                $branches{$branch}{"LAST"} = $ref;
 271                $current_rev = $num+1 if $current_rev < $num+1;
 272        }
 273        close($B);
 274}
 275-d $git_dir
 276        or die "Could not create git subdir ($git_dir).\n";
 277
 278open BRANCHES,">>", "$git_dir/svn2git";
 279
 280
 281## cvsps output:
 282#---------------------
 283#PatchSet 314
 284#Date: 1999/09/18 13:03:59
 285#Author: wkoch
 286#Branch: STABLE-BRANCH-1-0
 287#Ancestor branch: HEAD
 288#Tag: (none)
 289#Log:
 290#    See ChangeLog: Sat Sep 18 13:03:28 CEST 1999  Werner Koch
 291#Members:
 292#       README:1.57->1.57.2.1
 293#       VERSION:1.96->1.96.2.1
 294#
 295#---------------------
 296
 297my $state = 0;
 298
 299sub get_file($$$) {
 300        my($rev,$branch,$path) = @_;
 301
 302        # revert split_path(), below
 303        my $svnpath;
 304        $path = "" if $path eq "/"; # this should not happen, but ...
 305        if($branch eq "/") {
 306                $svnpath = "/$trunk_name/$path";
 307        } elsif($branch =~ m#^/#) {
 308                $svnpath = "/$tag_name$branch/$path";
 309        } else {
 310                $svnpath = "/$branch_name/$branch/$path";
 311        }
 312
 313        # now get it
 314        my ($name, $res) = eval { $svn->file($svnpath,$rev); };
 315        return () unless defined $name;
 316
 317        open my $F, '-|', "git-hash-object -w $name"
 318                or die "Cannot create object: $!\n";
 319        my $sha = <$F>;
 320        chomp $sha;
 321        close $F;
 322        # my $mode = pmode($cvs->{'mode'});
 323        my $mode = "0644"; # SV does not seem to store any file modes
 324        return [$mode, $sha, $path];
 325}
 326
 327sub split_path($$) {
 328        my($rev,$path) = @_;
 329        my $branch;
 330
 331        if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) {
 332                $branch = "/$1";
 333        } elsif($path =~ s#^/\Q$trunk_name\E/?##) {
 334                $branch = "/";
 335        } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) {
 336                $branch = $1;
 337        } else {
 338                print STDERR "$rev: Unrecognized path: $path\n";
 339                return ()
 340        }
 341        $path = "/" if $path eq "";
 342        return ($branch,$path);
 343}
 344
 345sub commit {
 346        my($branch, $changed_paths, $revision, $author, $date, $message) = @_;
 347        my($author_name,$author_email,$dest);
 348        my(@old,@new);
 349
 350        if (not defined $author) {
 351                $author_name = $author_email = "unknown";
 352        } elsif ($author =~ /^(.*?)\s+<(.*)>$/) {
 353                ($author_name, $author_email) = ($1, $2);
 354        } else {
 355                $author =~ s/^<(.*)>$/$1/;
 356                $author_name = $author_email = $author;
 357        }
 358        $date = pdate($date);
 359
 360        my $tag;
 361        my $parent;
 362        if($branch eq "/") { # trunk
 363                $parent = $opt_o;
 364        } elsif($branch =~ m#^/(.+)#) { # tag
 365                $tag = 1;
 366                $parent = $1;
 367        } else { # "normal" branch
 368                # nothing to do
 369                $parent = $branch;
 370        }
 371        $dest = $parent;
 372
 373        my $prev = $changed_paths->{"/"};
 374        if($prev and $prev->action eq "A") {
 375                delete $changed_paths->{"/"};
 376                my $oldpath = $prev->copyfrom_path;
 377                my $rev;
 378                if(defined $oldpath) {
 379                        my $p;
 380                        ($parent,$p) = split_path($revision,$oldpath);
 381                        if($parent eq "/") {
 382                                $parent = $opt_o;
 383                        } else {
 384                                $parent =~ s#^/##; # if it's a tag
 385                        }
 386                } else {
 387                        $parent = undef;
 388                }
 389        }
 390
 391        my $rev;
 392        if(defined $parent) {
 393                open(H,"git-rev-parse --verify $parent |");
 394                $rev = <H>;
 395                close(H) or do {
 396                        print STDERR "$revision: cannot find commit '$parent'!\n";
 397                        return;
 398                };
 399                chop $rev;
 400                if(length($rev) != 40) {
 401                        print STDERR "$revision: cannot find commit '$parent'!\n";
 402                        return;
 403                }
 404                $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"};
 405                if($revision != 1 and not $rev) {
 406                        print STDERR "$revision: do not know ancestor for '$parent'!\n";
 407                        return;
 408                }
 409        } else {
 410                $rev = undef;
 411        }
 412
 413#       if($prev and $prev->action eq "A") {
 414#               if(not $tag) {
 415#                       unless(open(H,"> $git_dir/refs/heads/$branch")) {
 416#                               print STDERR "$revision: Could not create branch $branch: $!\n";
 417#                               $state=11;
 418#                               next;
 419#                       }
 420#                       print H "$rev\n"
 421#                               or die "Could not write branch $branch: $!";
 422#                       close(H)
 423#                               or die "Could not write branch $branch: $!";
 424#               }
 425#       }
 426        if(not defined $rev) {
 427                unlink($git_index);
 428        } elsif ($rev ne $last_rev) {
 429                print "Switching from $last_rev to $rev ($branch)\n" if $opt_v;
 430                system("git-read-tree", $rev);
 431                die "read-tree failed for $rev: $?\n" if $?;
 432                $last_rev = $rev;
 433        }
 434
 435        while(my($path,$action) = each %$changed_paths) {
 436                if ($action->action eq "A") {
 437                        my $f = get_file($revision,$branch,$path);
 438                        push(@new,$f) if $f;
 439                } elsif ($action->action eq "D") {
 440                        push(@old,$path);
 441                } elsif ($action->action eq "M") {
 442                        my $f = get_file($revision,$branch,$path);
 443                        push(@new,$f) if $f;
 444                } elsif ($action->action eq "R") {
 445                        # refer to a file/tree in an earlier commit
 446                        push(@old,$path); # remove any old stuff
 447
 448                        # ... and add any new stuff
 449                        my($b,$p) = split_path($revision,$action->oldpath);
 450                        open my $F,"-|","git-ls-tree","-r","-z", $branches{$b}{$action->oldrev}, $p;
 451                        local $/ = '\0';
 452                        while(<$F>) {
 453                                chomp;
 454                                my($m,$p) = split(/\t/,$_,2);
 455                                my($mode,$type,$sha1) = split(/ /,$m);
 456                                next if $type ne "blob";
 457                                push(@new,[$mode,$sha1,$p]);
 458                        }
 459                } else {
 460                        die "$revision: unknown action '".$action->action."' for $path\n";
 461                }
 462        }
 463
 464        if(@old) {
 465                open F, "-│", "git-ls-files", "-z", @old or die $!;
 466                @old = ();
 467                local $/ = '\0';
 468                while(<F>) {
 469                        chomp;
 470                        push(@old,$_);
 471                }
 472                close(F);
 473
 474                while(@old) {
 475                        my @o2;
 476                        if(@old > 55) {
 477                                @o2 = splice(@old,0,50);
 478                        } else {
 479                                @o2 = @old;
 480                                @old = ();
 481                        }
 482                        system("git-update-index","--force-remove","--",@o2);
 483                        die "Cannot remove files: $?\n" if $?;
 484                }
 485        }
 486        while(@new) {
 487                my @n2;
 488                if(@new > 12) {
 489                        @n2 = splice(@new,0,10);
 490                } else {
 491                        @n2 = @new;
 492                        @new = ();
 493                }
 494                system("git-update-index","--add",
 495                        (map { ('--cacheinfo', @$_) } @n2));
 496                die "Cannot add files: $?\n" if $?;
 497        }
 498
 499        my $pid = open(C,"-|");
 500        die "Cannot fork: $!" unless defined $pid;
 501        unless($pid) {
 502                exec("git-write-tree");
 503                die "Cannot exec git-write-tree: $!\n";
 504        }
 505        chomp(my $tree = <C>);
 506        length($tree) == 40
 507                or die "Cannot get tree id ($tree): $!\n";
 508        close(C)
 509                or die "Error running git-write-tree: $?\n";
 510        print "Tree ID $tree\n" if $opt_v;
 511
 512        my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n";
 513        my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n";
 514        $pid = fork();
 515        die "Fork: $!\n" unless defined $pid;
 516        unless($pid) {
 517                $pr->writer();
 518                $pw->reader();
 519                open(OUT,">&STDOUT");
 520                dup2($pw->fileno(),0);
 521                dup2($pr->fileno(),1);
 522                $pr->close();
 523                $pw->close();
 524
 525                my @par = ();
 526                @par = ("-p",$rev) if defined $rev;
 527
 528                # loose detection of merges
 529                # based on the commit msg
 530                foreach my $rx (@mergerx) {
 531                        if ($message =~ $rx) {
 532                                my $mparent = $1;
 533                                if ($mparent eq 'HEAD') { $mparent = $opt_o };
 534                                if ( -e "$git_dir/refs/heads/$mparent") {
 535                                        $mparent = get_headref($mparent, $git_dir);
 536                                        push @par, '-p', $mparent;
 537                                        print OUT "Merge parent branch: $mparent\n" if $opt_v;
 538                                }
 539                        } 
 540                }
 541
 542                exec("env",
 543                        "GIT_AUTHOR_NAME=$author_name",
 544                        "GIT_AUTHOR_EMAIL=$author_email",
 545                        "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
 546                        "GIT_COMMITTER_NAME=$author_name",
 547                        "GIT_COMMITTER_EMAIL=$author_email",
 548                        "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)),
 549                        "git-commit-tree", $tree,@par);
 550                die "Cannot exec git-commit-tree: $!\n";
 551        }
 552        $pw->writer();
 553        $pr->reader();
 554
 555        $message =~ s/[\s\n]+\z//;
 556
 557        print $pw "$message\n"
 558                or die "Error writing to git-commit-tree: $!\n";
 559        $pw->close();
 560
 561        print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v;
 562        chomp(my $cid = <$pr>);
 563        length($cid) == 40
 564                or die "Cannot get commit id ($cid): $!\n";
 565        print "Commit ID $cid\n" if $opt_v;
 566        $pr->close();
 567
 568        waitpid($pid,0);
 569        die "Error running git-commit-tree: $?\n" if $?;
 570
 571        if(defined $dest) {
 572                print "Writing to refs/heads/$dest\n" if $opt_v;
 573                open(C,">$git_dir/refs/heads/$dest") and 
 574                print C ("$cid\n") and
 575                close(C)
 576                        or die "Cannot write branch $dest for update: $!\n";
 577        } else {
 578                print "... no known parent\n" if $opt_v;
 579        }
 580        $branches{$branch}{"LAST"} = $cid;
 581        $branches{$branch}{$revision} = $cid;
 582        $last_rev = $cid;
 583        print BRANCHES "$revision $branch $cid\n";
 584        print "DONE: $revision $dest $cid\n" if $opt_v;
 585
 586        if($tag) {
 587                my($in, $out) = ('','');
 588                $last_rev = "-" if %$changed_paths;
 589                # the tag was 'complex', i.e. did not refer to a "real" revision
 590                
 591                $tag =~ tr/_/\./ if $opt_u;
 592
 593                my $pid = open2($in, $out, 'git-mktag');
 594                print $out ("object $cid\n".
 595                    "type commit\n".
 596                    "tag $tag\n".
 597                    "tagger $author_name <$author_email>\n") and
 598                close($out)
 599                    or die "Cannot create tag object $tag: $!\n";
 600
 601                my $tagobj = <$in>;
 602                chomp $tagobj;
 603
 604                if ( !close($in) or waitpid($pid, 0) != $pid or
 605                                $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) {
 606                        die "Cannot create tag object $tag: $!\n";
 607                }
 608                
 609
 610                open(C,">$git_dir/refs/tags/$tag")
 611                        or die "Cannot create tag $tag: $!\n";
 612                print C "$tagobj\n"
 613                        or die "Cannot write tag $tag: $!\n";
 614                close(C)
 615                        or die "Cannot write tag $tag: $!\n";
 616
 617                print "Created tag '$tag' on '$branch'\n" if $opt_v;
 618        }
 619}
 620
 621my ($changed_paths, $revision, $author, $date, $message, $pool) = @_;
 622sub _commit_all {
 623        ($changed_paths, $revision, $author, $date, $message, $pool) = @_;
 624}
 625sub commit_all {
 626        my %done;
 627        my @col;
 628        my $pref;
 629        my $branch;
 630
 631        while(my($path,$action) = each %$changed_paths) {
 632                ($branch,$path) = split_path($revision,$path);
 633                next if not defined $branch;
 634                $done{$branch}{$path} = $action;
 635        }
 636        while(($branch,$changed_paths) = each %done) {
 637                commit($branch, $changed_paths, $revision, $author, $date, $message);
 638        }
 639}
 640
 641while(++$current_rev < $svn->{'maxrev'}) {
 642        $svn->{'svn'}->get_log("/",$current_rev,$current_rev,$current_rev,1,1,\&_commit_all,"");
 643        commit_all();
 644}
 645
 646
 647unlink($git_index);
 648
 649if (defined $orig_git_index) {
 650        $ENV{GIT_INDEX_FILE} = $orig_git_index;
 651} else {
 652        delete $ENV{GIT_INDEX_FILE};
 653}
 654
 655# Now switch back to the branch we were in before all of this happened
 656if($orig_branch) {
 657        print "DONE\n" if $opt_v;
 658        system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
 659                if $forward_master;
 660        unless ($opt_i) {
 661                system('git-read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD');
 662                die "read-tree failed: $?\n" if $?;
 663        }
 664} else {
 665        $orig_branch = "master";
 666        print "DONE; creating $orig_branch branch\n" if $opt_v;
 667        system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master")
 668                unless -f "$git_dir/refs/heads/master";
 669        unlink("$git_dir/HEAD");
 670        symlink("refs/heads/$orig_branch","$git_dir/HEAD");
 671        unless ($opt_i) {
 672                system('git checkout');
 673                die "checkout failed: $?\n" if $?;
 674        }
 675}
 676unlink("$git_dir/SVN2GIT_HEAD");
 677close(BRANCHES);