PageRenderTime 68ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/git-svn.perl

https://github.com/rrimando/git
Perl | 2113 lines | 1985 code | 88 blank | 40 comment | 157 complexity | e9e407f377c96f673b17a4aa5a319646 MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0, BSD-2-Clause, GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. #!/usr/bin/perl
  2. # Copyright (C) 2006, Eric Wong <normalperson@yhbt.net>
  3. # License: GPL v2 or later
  4. use 5.008;
  5. use warnings;
  6. use strict;
  7. use vars qw/ $AUTHOR $VERSION
  8. $sha1 $sha1_short $_revision $_repository
  9. $_q $_authors $_authors_prog %users/;
  10. $AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
  11. $VERSION = '@@GIT_VERSION@@';
  12. use Carp qw/croak/;
  13. use Digest::MD5;
  14. use IO::File qw//;
  15. use File::Basename qw/dirname basename/;
  16. use File::Path qw/mkpath/;
  17. use File::Spec;
  18. use File::Find;
  19. use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
  20. use IPC::Open3;
  21. use Memoize;
  22. use Git::SVN;
  23. use Git::SVN::Editor;
  24. use Git::SVN::Fetcher;
  25. use Git::SVN::Ra;
  26. use Git::SVN::Prompt;
  27. use Git::SVN::Log;
  28. use Git::SVN::Migration;
  29. use Git::SVN::Utils qw(
  30. fatal
  31. can_compress
  32. canonicalize_path
  33. canonicalize_url
  34. join_paths
  35. add_path_to_url
  36. join_paths
  37. );
  38. use Git qw(
  39. git_cmd_try
  40. command
  41. command_oneline
  42. command_noisy
  43. command_output_pipe
  44. command_close_pipe
  45. command_bidi_pipe
  46. command_close_bidi_pipe
  47. );
  48. BEGIN {
  49. Memoize::memoize 'Git::config';
  50. Memoize::memoize 'Git::config_bool';
  51. }
  52. # From which subdir have we been invoked?
  53. my $cmd_dir_prefix = eval {
  54. command_oneline([qw/rev-parse --show-prefix/], STDERR => 0)
  55. } || '';
  56. $Git::SVN::Ra::_log_window_size = 100;
  57. if (! exists $ENV{SVN_SSH} && exists $ENV{GIT_SSH}) {
  58. $ENV{SVN_SSH} = $ENV{GIT_SSH};
  59. }
  60. if (exists $ENV{SVN_SSH} && $^O eq 'msys') {
  61. $ENV{SVN_SSH} =~ s/\\/\\\\/g;
  62. $ENV{SVN_SSH} =~ s/(.*)/"$1"/;
  63. }
  64. $Git::SVN::Log::TZ = $ENV{TZ};
  65. $ENV{TZ} = 'UTC';
  66. $| = 1; # unbuffer STDOUT
  67. # All SVN commands do it. Otherwise we may die on SIGPIPE when the remote
  68. # repository decides to close the connection which we expect to be kept alive.
  69. $SIG{PIPE} = 'IGNORE';
  70. # Given a dot separated version number, "subtract" it from
  71. # the SVN::Core::VERSION; non-negaitive return means the SVN::Core
  72. # is at least at the version the caller asked for.
  73. sub compare_svn_version {
  74. my (@ours) = split(/\./, $SVN::Core::VERSION);
  75. my (@theirs) = split(/\./, $_[0]);
  76. my ($i, $diff);
  77. for ($i = 0; $i < @ours && $i < @theirs; $i++) {
  78. $diff = $ours[$i] - $theirs[$i];
  79. return $diff if ($diff);
  80. }
  81. return 1 if ($i < @ours);
  82. return -1 if ($i < @theirs);
  83. return 0;
  84. }
  85. sub _req_svn {
  86. require SVN::Core; # use()-ing this causes segfaults for me... *shrug*
  87. require SVN::Ra;
  88. require SVN::Delta;
  89. if (::compare_svn_version('1.1.0') < 0) {
  90. fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)";
  91. }
  92. }
  93. $sha1 = qr/[a-f\d]{40}/;
  94. $sha1_short = qr/[a-f\d]{4,40}/;
  95. my ($_stdin, $_help, $_edit,
  96. $_message, $_file, $_branch_dest,
  97. $_template, $_shared,
  98. $_version, $_fetch_all, $_no_rebase, $_fetch_parent,
  99. $_before, $_after,
  100. $_merge, $_strategy, $_preserve_merges, $_dry_run, $_local,
  101. $_prefix, $_no_checkout, $_url, $_verbose,
  102. $_commit_url, $_tag, $_merge_info, $_interactive);
  103. # This is a refactoring artifact so Git::SVN can get at this git-svn switch.
  104. sub opt_prefix { return $_prefix || '' }
  105. $Git::SVN::Fetcher::_placeholder_filename = ".gitignore";
  106. $_q ||= 0;
  107. my %remote_opts = ( 'username=s' => \$Git::SVN::Prompt::_username,
  108. 'config-dir=s' => \$Git::SVN::Ra::config_dir,
  109. 'no-auth-cache' => \$Git::SVN::Prompt::_no_auth_cache,
  110. 'ignore-paths=s' => \$Git::SVN::Fetcher::_ignore_regex,
  111. 'ignore-refs=s' => \$Git::SVN::Ra::_ignore_refs_regex );
  112. my %fc_opts = ( 'follow-parent|follow!' => \$Git::SVN::_follow_parent,
  113. 'authors-file|A=s' => \$_authors,
  114. 'authors-prog=s' => \$_authors_prog,
  115. 'repack:i' => \$Git::SVN::_repack,
  116. 'noMetadata' => \$Git::SVN::_no_metadata,
  117. 'useSvmProps' => \$Git::SVN::_use_svm_props,
  118. 'useSvnsyncProps' => \$Git::SVN::_use_svnsync_props,
  119. 'log-window-size=i' => \$Git::SVN::Ra::_log_window_size,
  120. 'no-checkout' => \$_no_checkout,
  121. 'quiet|q+' => \$_q,
  122. 'repack-flags|repack-args|repack-opts=s' =>
  123. \$Git::SVN::_repack_flags,
  124. 'use-log-author' => \$Git::SVN::_use_log_author,
  125. 'add-author-from' => \$Git::SVN::_add_author_from,
  126. 'localtime' => \$Git::SVN::_localtime,
  127. %remote_opts );
  128. my ($_trunk, @_tags, @_branches, $_stdlayout);
  129. my %icv;
  130. my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared,
  131. 'trunk|T=s' => \$_trunk, 'tags|t=s@' => \@_tags,
  132. 'branches|b=s@' => \@_branches, 'prefix=s' => \$_prefix,
  133. 'stdlayout|s' => \$_stdlayout,
  134. 'minimize-url|m!' => \$Git::SVN::_minimize_url,
  135. 'no-metadata' => sub { $icv{noMetadata} = 1 },
  136. 'use-svm-props' => sub { $icv{useSvmProps} = 1 },
  137. 'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 },
  138. 'rewrite-root=s' => sub { $icv{rewriteRoot} = $_[1] },
  139. 'rewrite-uuid=s' => sub { $icv{rewriteUUID} = $_[1] },
  140. %remote_opts );
  141. my %cmt_opts = ( 'edit|e' => \$_edit,
  142. 'rmdir' => \$Git::SVN::Editor::_rmdir,
  143. 'find-copies-harder' => \$Git::SVN::Editor::_find_copies_harder,
  144. 'l=i' => \$Git::SVN::Editor::_rename_limit,
  145. 'copy-similarity|C=i'=> \$Git::SVN::Editor::_cp_similarity
  146. );
  147. my %cmd = (
  148. fetch => [ \&cmd_fetch, "Download new revisions from SVN",
  149. { 'revision|r=s' => \$_revision,
  150. 'fetch-all|all' => \$_fetch_all,
  151. 'parent|p' => \$_fetch_parent,
  152. %fc_opts } ],
  153. clone => [ \&cmd_clone, "Initialize and fetch revisions",
  154. { 'revision|r=s' => \$_revision,
  155. 'preserve-empty-dirs' =>
  156. \$Git::SVN::Fetcher::_preserve_empty_dirs,
  157. 'placeholder-filename=s' =>
  158. \$Git::SVN::Fetcher::_placeholder_filename,
  159. %fc_opts, %init_opts } ],
  160. init => [ \&cmd_init, "Initialize a repo for tracking" .
  161. " (requires URL argument)",
  162. \%init_opts ],
  163. 'multi-init' => [ \&cmd_multi_init,
  164. "Deprecated alias for ".
  165. "'$0 init -T<trunk> -b<branches> -t<tags>'",
  166. \%init_opts ],
  167. dcommit => [ \&cmd_dcommit,
  168. 'Commit several diffs to merge with upstream',
  169. { 'merge|m|M' => \$_merge,
  170. 'strategy|s=s' => \$_strategy,
  171. 'verbose|v' => \$_verbose,
  172. 'dry-run|n' => \$_dry_run,
  173. 'fetch-all|all' => \$_fetch_all,
  174. 'commit-url=s' => \$_commit_url,
  175. 'revision|r=i' => \$_revision,
  176. 'no-rebase' => \$_no_rebase,
  177. 'mergeinfo=s' => \$_merge_info,
  178. 'interactive|i' => \$_interactive,
  179. %cmt_opts, %fc_opts } ],
  180. branch => [ \&cmd_branch,
  181. 'Create a branch in the SVN repository',
  182. { 'message|m=s' => \$_message,
  183. 'destination|d=s' => \$_branch_dest,
  184. 'dry-run|n' => \$_dry_run,
  185. 'tag|t' => \$_tag,
  186. 'username=s' => \$Git::SVN::Prompt::_username,
  187. 'commit-url=s' => \$_commit_url } ],
  188. tag => [ sub { $_tag = 1; cmd_branch(@_) },
  189. 'Create a tag in the SVN repository',
  190. { 'message|m=s' => \$_message,
  191. 'destination|d=s' => \$_branch_dest,
  192. 'dry-run|n' => \$_dry_run,
  193. 'username=s' => \$Git::SVN::Prompt::_username,
  194. 'commit-url=s' => \$_commit_url } ],
  195. 'set-tree' => [ \&cmd_set_tree,
  196. "Set an SVN repository to a git tree-ish",
  197. { 'stdin' => \$_stdin, %cmt_opts, %fc_opts, } ],
  198. 'create-ignore' => [ \&cmd_create_ignore,
  199. 'Create a .gitignore per svn:ignore',
  200. { 'revision|r=i' => \$_revision
  201. } ],
  202. 'mkdirs' => [ \&cmd_mkdirs ,
  203. "recreate empty directories after a checkout",
  204. { 'revision|r=i' => \$_revision } ],
  205. 'propget' => [ \&cmd_propget,
  206. 'Print the value of a property on a file or directory',
  207. { 'revision|r=i' => \$_revision } ],
  208. 'proplist' => [ \&cmd_proplist,
  209. 'List all properties of a file or directory',
  210. { 'revision|r=i' => \$_revision } ],
  211. 'show-ignore' => [ \&cmd_show_ignore, "Show svn:ignore listings",
  212. { 'revision|r=i' => \$_revision
  213. } ],
  214. 'show-externals' => [ \&cmd_show_externals, "Show svn:externals listings",
  215. { 'revision|r=i' => \$_revision
  216. } ],
  217. 'multi-fetch' => [ \&cmd_multi_fetch,
  218. "Deprecated alias for $0 fetch --all",
  219. { 'revision|r=s' => \$_revision, %fc_opts } ],
  220. 'migrate' => [ sub { },
  221. # no-op, we automatically run this anyways,
  222. 'Migrate configuration/metadata/layout from
  223. previous versions of git-svn',
  224. { 'minimize' => \$Git::SVN::Migration::_minimize,
  225. %remote_opts } ],
  226. 'log' => [ \&Git::SVN::Log::cmd_show_log, 'Show commit logs',
  227. { 'limit=i' => \$Git::SVN::Log::limit,
  228. 'revision|r=s' => \$_revision,
  229. 'verbose|v' => \$Git::SVN::Log::verbose,
  230. 'incremental' => \$Git::SVN::Log::incremental,
  231. 'oneline' => \$Git::SVN::Log::oneline,
  232. 'show-commit' => \$Git::SVN::Log::show_commit,
  233. 'non-recursive' => \$Git::SVN::Log::non_recursive,
  234. 'authors-file|A=s' => \$_authors,
  235. 'color' => \$Git::SVN::Log::color,
  236. 'pager=s' => \$Git::SVN::Log::pager
  237. } ],
  238. 'find-rev' => [ \&cmd_find_rev,
  239. "Translate between SVN revision numbers and tree-ish",
  240. { 'before' => \$_before,
  241. 'after' => \$_after } ],
  242. 'rebase' => [ \&cmd_rebase, "Fetch and rebase your working directory",
  243. { 'merge|m|M' => \$_merge,
  244. 'verbose|v' => \$_verbose,
  245. 'strategy|s=s' => \$_strategy,
  246. 'local|l' => \$_local,
  247. 'fetch-all|all' => \$_fetch_all,
  248. 'dry-run|n' => \$_dry_run,
  249. 'preserve-merges|p' => \$_preserve_merges,
  250. %fc_opts } ],
  251. 'commit-diff' => [ \&cmd_commit_diff,
  252. 'Commit a diff between two trees',
  253. { 'message|m=s' => \$_message,
  254. 'file|F=s' => \$_file,
  255. 'revision|r=s' => \$_revision,
  256. %cmt_opts } ],
  257. 'info' => [ \&cmd_info,
  258. "Show info about the latest SVN revision
  259. on the current branch",
  260. { 'url' => \$_url, } ],
  261. 'blame' => [ \&Git::SVN::Log::cmd_blame,
  262. "Show what revision and author last modified each line of a file",
  263. { 'git-format' => \$Git::SVN::Log::_git_format } ],
  264. 'reset' => [ \&cmd_reset,
  265. "Undo fetches back to the specified SVN revision",
  266. { 'revision|r=s' => \$_revision,
  267. 'parent|p' => \$_fetch_parent } ],
  268. 'gc' => [ \&cmd_gc,
  269. "Compress unhandled.log files in .git/svn and remove " .
  270. "index files in .git/svn",
  271. {} ],
  272. );
  273. use Term::ReadLine;
  274. package FakeTerm;
  275. sub new {
  276. my ($class, $reason) = @_;
  277. return bless \$reason, shift;
  278. }
  279. sub readline {
  280. my $self = shift;
  281. die "Cannot use readline on FakeTerm: $$self";
  282. }
  283. package main;
  284. my $term = eval {
  285. $ENV{"GIT_SVN_NOTTY"}
  286. ? new Term::ReadLine 'git-svn', \*STDIN, \*STDOUT
  287. : new Term::ReadLine 'git-svn';
  288. };
  289. if ($@) {
  290. $term = new FakeTerm "$@: going non-interactive";
  291. }
  292. my $cmd;
  293. for (my $i = 0; $i < @ARGV; $i++) {
  294. if (defined $cmd{$ARGV[$i]}) {
  295. $cmd = $ARGV[$i];
  296. splice @ARGV, $i, 1;
  297. last;
  298. } elsif ($ARGV[$i] eq 'help') {
  299. $cmd = $ARGV[$i+1];
  300. usage(0);
  301. }
  302. };
  303. # make sure we're always running at the top-level working directory
  304. if ($cmd && $cmd =~ /(?:clone|init|multi-init)$/) {
  305. $ENV{GIT_DIR} ||= ".git";
  306. } else {
  307. my ($git_dir, $cdup);
  308. git_cmd_try {
  309. $git_dir = command_oneline([qw/rev-parse --git-dir/]);
  310. } "Unable to find .git directory\n";
  311. git_cmd_try {
  312. $cdup = command_oneline(qw/rev-parse --show-cdup/);
  313. chomp $cdup if ($cdup);
  314. $cdup = "." unless ($cdup && length $cdup);
  315. } "Already at toplevel, but $git_dir not found\n";
  316. $ENV{GIT_DIR} = $git_dir;
  317. chdir $cdup or die "Unable to chdir up to '$cdup'\n";
  318. $_repository = Git->repository(Repository => $ENV{GIT_DIR});
  319. }
  320. my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
  321. read_git_config(\%opts);
  322. if ($cmd && ($cmd eq 'log' || $cmd eq 'blame')) {
  323. Getopt::Long::Configure('pass_through');
  324. }
  325. my $rv = GetOptions(%opts, 'h|H' => \$_help, 'version|V' => \$_version,
  326. 'minimize-connections' => \$Git::SVN::Migration::_minimize,
  327. 'id|i=s' => \$Git::SVN::default_ref_id,
  328. 'svn-remote|remote|R=s' => sub {
  329. $Git::SVN::no_reuse_existing = 1;
  330. $Git::SVN::default_repo_id = $_[1] });
  331. exit 1 if (!$rv && $cmd && $cmd ne 'log');
  332. usage(0) if $_help;
  333. version() if $_version;
  334. usage(1) unless defined $cmd;
  335. load_authors() if $_authors;
  336. if (defined $_authors_prog) {
  337. $_authors_prog = "'" . File::Spec->rel2abs($_authors_prog) . "'";
  338. }
  339. unless ($cmd =~ /^(?:clone|init|multi-init|commit-diff)$/) {
  340. Git::SVN::Migration::migration_check();
  341. }
  342. Git::SVN::init_vars();
  343. eval {
  344. Git::SVN::verify_remotes_sanity();
  345. $cmd{$cmd}->[0]->(@ARGV);
  346. post_fetch_checkout();
  347. };
  348. fatal $@ if $@;
  349. exit 0;
  350. ####################### primary functions ######################
  351. sub usage {
  352. my $exit = shift || 0;
  353. my $fd = $exit ? \*STDERR : \*STDOUT;
  354. print $fd <<"";
  355. git-svn - bidirectional operations between a single Subversion tree and git
  356. Usage: git svn <command> [options] [arguments]\n
  357. print $fd "Available commands:\n" unless $cmd;
  358. foreach (sort keys %cmd) {
  359. next if $cmd && $cmd ne $_;
  360. next if /^multi-/; # don't show deprecated commands
  361. print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n";
  362. foreach (sort keys %{$cmd{$_}->[2]}) {
  363. # mixed-case options are for .git/config only
  364. next if /[A-Z]/ && /^[a-z]+$/i;
  365. # prints out arguments as they should be passed:
  366. my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
  367. print $fd ' ' x 21, join(', ', map { length $_ > 1 ?
  368. "--$_" : "-$_" }
  369. split /\|/,$_)," $x\n";
  370. }
  371. }
  372. print $fd <<"";
  373. \nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an
  374. arbitrary identifier if you're tracking multiple SVN branches/repositories in
  375. one git repository and want to keep them separate. See git-svn(1) for more
  376. information.
  377. exit $exit;
  378. }
  379. sub version {
  380. ::_req_svn();
  381. print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n";
  382. exit 0;
  383. }
  384. sub ask {
  385. my ($prompt, %arg) = @_;
  386. my $valid_re = $arg{valid_re};
  387. my $default = $arg{default};
  388. my $resp;
  389. my $i = 0;
  390. if ( !( defined($term->IN)
  391. && defined( fileno($term->IN) )
  392. && defined( $term->OUT )
  393. && defined( fileno($term->OUT) ) ) ){
  394. return defined($default) ? $default : undef;
  395. }
  396. while ($i++ < 10) {
  397. $resp = $term->readline($prompt);
  398. if (!defined $resp) { # EOF
  399. print "\n";
  400. return defined $default ? $default : undef;
  401. }
  402. if ($resp eq '' and defined $default) {
  403. return $default;
  404. }
  405. if (!defined $valid_re or $resp =~ /$valid_re/) {
  406. return $resp;
  407. }
  408. }
  409. return undef;
  410. }
  411. sub do_git_init_db {
  412. unless (-d $ENV{GIT_DIR}) {
  413. my @init_db = ('init');
  414. push @init_db, "--template=$_template" if defined $_template;
  415. if (defined $_shared) {
  416. if ($_shared =~ /[a-z]/) {
  417. push @init_db, "--shared=$_shared";
  418. } else {
  419. push @init_db, "--shared";
  420. }
  421. }
  422. command_noisy(@init_db);
  423. $_repository = Git->repository(Repository => ".git");
  424. }
  425. my $set;
  426. my $pfx = "svn-remote.$Git::SVN::default_repo_id";
  427. foreach my $i (keys %icv) {
  428. die "'$set' and '$i' cannot both be set\n" if $set;
  429. next unless defined $icv{$i};
  430. command_noisy('config', "$pfx.$i", $icv{$i});
  431. $set = $i;
  432. }
  433. my $ignore_paths_regex = \$Git::SVN::Fetcher::_ignore_regex;
  434. command_noisy('config', "$pfx.ignore-paths", $$ignore_paths_regex)
  435. if defined $$ignore_paths_regex;
  436. my $ignore_refs_regex = \$Git::SVN::Ra::_ignore_refs_regex;
  437. command_noisy('config', "$pfx.ignore-refs", $$ignore_refs_regex)
  438. if defined $$ignore_refs_regex;
  439. if (defined $Git::SVN::Fetcher::_preserve_empty_dirs) {
  440. my $fname = \$Git::SVN::Fetcher::_placeholder_filename;
  441. command_noisy('config', "$pfx.preserve-empty-dirs", 'true');
  442. command_noisy('config', "$pfx.placeholder-filename", $$fname);
  443. }
  444. }
  445. sub init_subdir {
  446. my $repo_path = shift or return;
  447. mkpath([$repo_path]) unless -d $repo_path;
  448. chdir $repo_path or die "Couldn't chdir to $repo_path: $!\n";
  449. $ENV{GIT_DIR} = '.git';
  450. $_repository = Git->repository(Repository => $ENV{GIT_DIR});
  451. }
  452. sub cmd_clone {
  453. my ($url, $path) = @_;
  454. if (!defined $path &&
  455. (defined $_trunk || @_branches || @_tags ||
  456. defined $_stdlayout) &&
  457. $url !~ m#^[a-z\+]+://#) {
  458. $path = $url;
  459. }
  460. $path = basename($url) if !defined $path || !length $path;
  461. my $authors_absolute = $_authors ? File::Spec->rel2abs($_authors) : "";
  462. cmd_init($url, $path);
  463. command_oneline('config', 'svn.authorsfile', $authors_absolute)
  464. if $_authors;
  465. Git::SVN::fetch_all($Git::SVN::default_repo_id);
  466. }
  467. sub cmd_init {
  468. if (defined $_stdlayout) {
  469. $_trunk = 'trunk' if (!defined $_trunk);
  470. @_tags = 'tags' if (! @_tags);
  471. @_branches = 'branches' if (! @_branches);
  472. }
  473. if (defined $_trunk || @_branches || @_tags) {
  474. return cmd_multi_init(@_);
  475. }
  476. my $url = shift or die "SVN repository location required ",
  477. "as a command-line argument\n";
  478. $url = canonicalize_url($url);
  479. init_subdir(@_);
  480. do_git_init_db();
  481. if ($Git::SVN::_minimize_url eq 'unset') {
  482. $Git::SVN::_minimize_url = 0;
  483. }
  484. Git::SVN->init($url);
  485. }
  486. sub cmd_fetch {
  487. if (grep /^\d+=./, @_) {
  488. die "'<rev>=<commit>' fetch arguments are ",
  489. "no longer supported.\n";
  490. }
  491. my ($remote) = @_;
  492. if (@_ > 1) {
  493. die "Usage: $0 fetch [--all] [--parent] [svn-remote]\n";
  494. }
  495. $Git::SVN::no_reuse_existing = undef;
  496. if ($_fetch_parent) {
  497. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  498. unless ($gs) {
  499. die "Unable to determine upstream SVN information from ",
  500. "working tree history\n";
  501. }
  502. # just fetch, don't checkout.
  503. $_no_checkout = 'true';
  504. $_fetch_all ? $gs->fetch_all : $gs->fetch;
  505. } elsif ($_fetch_all) {
  506. cmd_multi_fetch();
  507. } else {
  508. $remote ||= $Git::SVN::default_repo_id;
  509. Git::SVN::fetch_all($remote, Git::SVN::read_all_remotes());
  510. }
  511. }
  512. sub cmd_set_tree {
  513. my (@commits) = @_;
  514. if ($_stdin || !@commits) {
  515. print "Reading from stdin...\n";
  516. @commits = ();
  517. while (<STDIN>) {
  518. if (/\b($sha1_short)\b/o) {
  519. unshift @commits, $1;
  520. }
  521. }
  522. }
  523. my @revs;
  524. foreach my $c (@commits) {
  525. my @tmp = command('rev-parse',$c);
  526. if (scalar @tmp == 1) {
  527. push @revs, $tmp[0];
  528. } elsif (scalar @tmp > 1) {
  529. push @revs, reverse(command('rev-list',@tmp));
  530. } else {
  531. fatal "Failed to rev-parse $c";
  532. }
  533. }
  534. my $gs = Git::SVN->new;
  535. my ($r_last, $cmt_last) = $gs->last_rev_commit;
  536. $gs->fetch;
  537. if (defined $gs->{last_rev} && $r_last != $gs->{last_rev}) {
  538. fatal "There are new revisions that were fetched ",
  539. "and need to be merged (or acknowledged) ",
  540. "before committing.\nlast rev: $r_last\n",
  541. " current: $gs->{last_rev}";
  542. }
  543. $gs->set_tree($_) foreach @revs;
  544. print "Done committing ",scalar @revs," revisions to SVN\n";
  545. unlink $gs->{index};
  546. }
  547. sub split_merge_info_range {
  548. my ($range) = @_;
  549. if ($range =~ /(\d+)-(\d+)/) {
  550. return (int($1), int($2));
  551. } else {
  552. return (int($range), int($range));
  553. }
  554. }
  555. sub combine_ranges {
  556. my ($in) = @_;
  557. my @fnums = ();
  558. my @arr = split(/,/, $in);
  559. for my $element (@arr) {
  560. my ($start, $end) = split_merge_info_range($element);
  561. push @fnums, $start;
  562. }
  563. my @sorted = @arr [ sort {
  564. $fnums[$a] <=> $fnums[$b]
  565. } 0..$#arr ];
  566. my @return = ();
  567. my $last = -1;
  568. my $first = -1;
  569. for my $element (@sorted) {
  570. my ($start, $end) = split_merge_info_range($element);
  571. if ($last == -1) {
  572. $first = $start;
  573. $last = $end;
  574. next;
  575. }
  576. if ($start <= $last+1) {
  577. if ($end > $last) {
  578. $last = $end;
  579. }
  580. next;
  581. }
  582. if ($first == $last) {
  583. push @return, "$first";
  584. } else {
  585. push @return, "$first-$last";
  586. }
  587. $first = $start;
  588. $last = $end;
  589. }
  590. if ($first != -1) {
  591. if ($first == $last) {
  592. push @return, "$first";
  593. } else {
  594. push @return, "$first-$last";
  595. }
  596. }
  597. return join(',', @return);
  598. }
  599. sub merge_revs_into_hash {
  600. my ($hash, $minfo) = @_;
  601. my @lines = split(' ', $minfo);
  602. for my $line (@lines) {
  603. my ($branchpath, $revs) = split(/:/, $line);
  604. if (exists($hash->{$branchpath})) {
  605. # Merge the two revision sets
  606. my $combined = "$hash->{$branchpath},$revs";
  607. $hash->{$branchpath} = combine_ranges($combined);
  608. } else {
  609. # Just do range combining for consolidation
  610. $hash->{$branchpath} = combine_ranges($revs);
  611. }
  612. }
  613. }
  614. sub merge_merge_info {
  615. my ($mergeinfo_one, $mergeinfo_two) = @_;
  616. my %result_hash = ();
  617. merge_revs_into_hash(\%result_hash, $mergeinfo_one);
  618. merge_revs_into_hash(\%result_hash, $mergeinfo_two);
  619. my $result = '';
  620. # Sort below is for consistency's sake
  621. for my $branchname (sort keys(%result_hash)) {
  622. my $revlist = $result_hash{$branchname};
  623. $result .= "$branchname:$revlist\n"
  624. }
  625. return $result;
  626. }
  627. sub populate_merge_info {
  628. my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_;
  629. my %parentshash;
  630. read_commit_parents(\%parentshash, $d);
  631. my @parents = @{$parentshash{$d}};
  632. if ($#parents > 0) {
  633. # Merge commit
  634. my $all_parents_ok = 1;
  635. my $aggregate_mergeinfo = '';
  636. my $rooturl = $gs->repos_root;
  637. if (defined($rewritten_parent)) {
  638. # Replace first parent with newly-rewritten version
  639. shift @parents;
  640. unshift @parents, $rewritten_parent;
  641. }
  642. foreach my $parent (@parents) {
  643. my ($branchurl, $svnrev, $paruuid) =
  644. cmt_metadata($parent);
  645. unless (defined($svnrev)) {
  646. # Should have been caught be preflight check
  647. fatal "merge commit $d has ancestor $parent, but that change "
  648. ."does not have git-svn metadata!";
  649. }
  650. unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
  651. fatal "commit $parent git-svn metadata changed mid-run!";
  652. }
  653. my $branchpath = $1;
  654. my $ra = Git::SVN::Ra->new($branchurl);
  655. my (undef, undef, $props) =
  656. $ra->get_dir(canonicalize_path("."), $svnrev);
  657. my $par_mergeinfo = $props->{'svn:mergeinfo'};
  658. unless (defined $par_mergeinfo) {
  659. $par_mergeinfo = '';
  660. }
  661. # Merge previous mergeinfo values
  662. $aggregate_mergeinfo =
  663. merge_merge_info($aggregate_mergeinfo,
  664. $par_mergeinfo, 0);
  665. next if $parent eq $parents[0]; # Skip first parent
  666. # Add new changes being placed in tree by merge
  667. my @cmd = (qw/rev-list --reverse/,
  668. $parent, qw/--not/);
  669. foreach my $par (@parents) {
  670. unless ($par eq $parent) {
  671. push @cmd, $par;
  672. }
  673. }
  674. my @revsin = ();
  675. my ($revlist, $ctx) = command_output_pipe(@cmd);
  676. while (<$revlist>) {
  677. my $irev = $_;
  678. chomp $irev;
  679. my (undef, $csvnrev, undef) =
  680. cmt_metadata($irev);
  681. unless (defined $csvnrev) {
  682. # A child is missing SVN annotations...
  683. # this might be OK, or might not be.
  684. warn "W:child $irev is merged into revision "
  685. ."$d but does not have git-svn metadata. "
  686. ."This means git-svn cannot determine the "
  687. ."svn revision numbers to place into the "
  688. ."svn:mergeinfo property. You must ensure "
  689. ."a branch is entirely committed to "
  690. ."SVN before merging it in order for "
  691. ."svn:mergeinfo population to function "
  692. ."properly";
  693. }
  694. push @revsin, $csvnrev;
  695. }
  696. command_close_pipe($revlist, $ctx);
  697. last unless $all_parents_ok;
  698. # We now have a list of all SVN revnos which are
  699. # merged by this particular parent. Integrate them.
  700. next if $#revsin == -1;
  701. my $newmergeinfo = "$branchpath:" . join(',', @revsin);
  702. $aggregate_mergeinfo =
  703. merge_merge_info($aggregate_mergeinfo,
  704. $newmergeinfo, 1);
  705. }
  706. if ($all_parents_ok and $aggregate_mergeinfo) {
  707. return $aggregate_mergeinfo;
  708. }
  709. }
  710. return undef;
  711. }
  712. sub dcommit_rebase {
  713. my ($is_last, $current, $fetched_ref, $svn_error) = @_;
  714. my @diff;
  715. if ($svn_error) {
  716. print STDERR "\nERROR from SVN:\n",
  717. $svn_error->expanded_message, "\n";
  718. }
  719. unless ($_no_rebase) {
  720. # we always want to rebase against the current HEAD,
  721. # not any head that was passed to us
  722. @diff = command('diff-tree', $current,
  723. $fetched_ref, '--');
  724. my @finish;
  725. if (@diff) {
  726. @finish = rebase_cmd();
  727. print STDERR "W: $current and ", $fetched_ref,
  728. " differ, using @finish:\n",
  729. join("\n", @diff), "\n";
  730. } elsif ($is_last) {
  731. print "No changes between ", $current, " and ",
  732. $fetched_ref,
  733. "\nResetting to the latest ",
  734. $fetched_ref, "\n";
  735. @finish = qw/reset --mixed/;
  736. }
  737. command_noisy(@finish, $fetched_ref) if @finish;
  738. }
  739. if ($svn_error) {
  740. die "ERROR: Not all changes have been committed into SVN"
  741. .($_no_rebase ? ".\n" : ", however the committed\n"
  742. ."ones (if any) seem to be successfully integrated "
  743. ."into the working tree.\n")
  744. ."Please see the above messages for details.\n";
  745. }
  746. return @diff;
  747. }
  748. sub cmd_dcommit {
  749. my $head = shift;
  750. command_noisy(qw/update-index --refresh/);
  751. git_cmd_try { command_oneline(qw/diff-index --quiet HEAD/) }
  752. 'Cannot dcommit with a dirty index. Commit your changes first, '
  753. . "or stash them with `git stash'.\n";
  754. $head ||= 'HEAD';
  755. my $old_head;
  756. if ($head ne 'HEAD') {
  757. $old_head = eval {
  758. command_oneline([qw/symbolic-ref -q HEAD/])
  759. };
  760. if ($old_head) {
  761. $old_head =~ s{^refs/heads/}{};
  762. } else {
  763. $old_head = eval { command_oneline(qw/rev-parse HEAD/) };
  764. }
  765. command(['checkout', $head], STDERR => 0);
  766. }
  767. my @refs;
  768. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD', \@refs);
  769. unless ($gs) {
  770. die "Unable to determine upstream SVN information from ",
  771. "$head history.\nPerhaps the repository is empty.";
  772. }
  773. if (defined $_commit_url) {
  774. $url = $_commit_url;
  775. } else {
  776. $url = eval { command_oneline('config', '--get',
  777. "svn-remote.$gs->{repo_id}.commiturl") };
  778. if (!$url) {
  779. $url = $gs->full_pushurl
  780. }
  781. }
  782. my $last_rev = $_revision if defined $_revision;
  783. if ($url) {
  784. print "Committing to $url ...\n";
  785. }
  786. my ($linear_refs, $parents) = linearize_history($gs, \@refs);
  787. if ($_no_rebase && scalar(@$linear_refs) > 1) {
  788. warn "Attempting to commit more than one change while ",
  789. "--no-rebase is enabled.\n",
  790. "If these changes depend on each other, re-running ",
  791. "without --no-rebase may be required."
  792. }
  793. if (defined $_interactive){
  794. my $ask_default = "y";
  795. foreach my $d (@$linear_refs){
  796. my ($fh, $ctx) = command_output_pipe(qw(show --summary), "$d");
  797. while (<$fh>){
  798. print $_;
  799. }
  800. command_close_pipe($fh, $ctx);
  801. $_ = ask("Commit this patch to SVN? ([y]es (default)|[n]o|[q]uit|[a]ll): ",
  802. valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i,
  803. default => $ask_default);
  804. die "Commit this patch reply required" unless defined $_;
  805. if (/^[nq]/i) {
  806. exit(0);
  807. } elsif (/^a/i) {
  808. last;
  809. }
  810. }
  811. }
  812. my $expect_url = $url;
  813. my $push_merge_info = eval {
  814. command_oneline(qw/config --get svn.pushmergeinfo/)
  815. };
  816. if (not defined($push_merge_info)
  817. or $push_merge_info eq "false"
  818. or $push_merge_info eq "no"
  819. or $push_merge_info eq "never") {
  820. $push_merge_info = 0;
  821. }
  822. unless (defined($_merge_info) || ! $push_merge_info) {
  823. # Preflight check of changes to ensure no issues with mergeinfo
  824. # This includes check for uncommitted-to-SVN parents
  825. # (other than the first parent, which we will handle),
  826. # information from different SVN repos, and paths
  827. # which are not underneath this repository root.
  828. my $rooturl = $gs->repos_root;
  829. foreach my $d (@$linear_refs) {
  830. my %parentshash;
  831. read_commit_parents(\%parentshash, $d);
  832. my @realparents = @{$parentshash{$d}};
  833. if ($#realparents > 0) {
  834. # Merge commit
  835. shift @realparents; # Remove/ignore first parent
  836. foreach my $parent (@realparents) {
  837. my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent);
  838. unless (defined $paruuid) {
  839. # A parent is missing SVN annotations...
  840. # abort the whole operation.
  841. fatal "$parent is merged into revision $d, "
  842. ."but does not have git-svn metadata. "
  843. ."Either dcommit the branch or use a "
  844. ."local cherry-pick, FF merge, or rebase "
  845. ."instead of an explicit merge commit.";
  846. }
  847. unless ($paruuid eq $uuid) {
  848. # Parent has SVN metadata from different repository
  849. fatal "merge parent $parent for change $d has "
  850. ."git-svn uuid $paruuid, while current change "
  851. ."has uuid $uuid!";
  852. }
  853. unless ($branchurl =~ /^\Q$rooturl\E(.*)/) {
  854. # This branch is very strange indeed.
  855. fatal "merge parent $parent for $d is on branch "
  856. ."$branchurl, which is not under the "
  857. ."git-svn root $rooturl!";
  858. }
  859. }
  860. }
  861. }
  862. }
  863. my $rewritten_parent;
  864. my $current_head = command_oneline(qw/rev-parse HEAD/);
  865. Git::SVN::remove_username($expect_url);
  866. if (defined($_merge_info)) {
  867. $_merge_info =~ tr{ }{\n};
  868. }
  869. while (1) {
  870. my $d = shift @$linear_refs or last;
  871. unless (defined $last_rev) {
  872. (undef, $last_rev, undef) = cmt_metadata("$d~1");
  873. unless (defined $last_rev) {
  874. fatal "Unable to extract revision information ",
  875. "from commit $d~1";
  876. }
  877. }
  878. if ($_dry_run) {
  879. print "diff-tree $d~1 $d\n";
  880. } else {
  881. my $cmt_rev;
  882. unless (defined($_merge_info) || ! $push_merge_info) {
  883. $_merge_info = populate_merge_info($d, $gs,
  884. $uuid,
  885. $linear_refs,
  886. $rewritten_parent);
  887. }
  888. my %ed_opts = ( r => $last_rev,
  889. log => get_commit_entry($d)->{log},
  890. ra => Git::SVN::Ra->new($url),
  891. config => SVN::Core::config_get_config(
  892. $Git::SVN::Ra::config_dir
  893. ),
  894. tree_a => "$d~1",
  895. tree_b => $d,
  896. editor_cb => sub {
  897. print "Committed r$_[0]\n";
  898. $cmt_rev = $_[0];
  899. },
  900. mergeinfo => $_merge_info,
  901. svn_path => '');
  902. my $err_handler = $SVN::Error::handler;
  903. $SVN::Error::handler = sub {
  904. my $err = shift;
  905. dcommit_rebase(1, $current_head, $gs->refname,
  906. $err);
  907. };
  908. if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
  909. print "No changes\n$d~1 == $d\n";
  910. } elsif ($parents->{$d} && @{$parents->{$d}}) {
  911. $gs->{inject_parents_dcommit}->{$cmt_rev} =
  912. $parents->{$d};
  913. }
  914. $_fetch_all ? $gs->fetch_all : $gs->fetch;
  915. $SVN::Error::handler = $err_handler;
  916. $last_rev = $cmt_rev;
  917. next if $_no_rebase;
  918. my @diff = dcommit_rebase(@$linear_refs == 0, $d,
  919. $gs->refname, undef);
  920. $rewritten_parent = command_oneline(qw/rev-parse/,
  921. $gs->refname);
  922. if (@diff) {
  923. $current_head = command_oneline(qw/rev-parse
  924. HEAD/);
  925. @refs = ();
  926. my ($url_, $rev_, $uuid_, $gs_) =
  927. working_head_info('HEAD', \@refs);
  928. my ($linear_refs_, $parents_) =
  929. linearize_history($gs_, \@refs);
  930. if (scalar(@$linear_refs) !=
  931. scalar(@$linear_refs_)) {
  932. fatal "# of revisions changed ",
  933. "\nbefore:\n",
  934. join("\n", @$linear_refs),
  935. "\n\nafter:\n",
  936. join("\n", @$linear_refs_), "\n",
  937. 'If you are attempting to commit ',
  938. "merges, try running:\n\t",
  939. 'git rebase --interactive',
  940. '--preserve-merges ',
  941. $gs->refname,
  942. "\nBefore dcommitting";
  943. }
  944. if ($url_ ne $expect_url) {
  945. if ($url_ eq $gs->metadata_url) {
  946. print
  947. "Accepting rewritten URL:",
  948. " $url_\n";
  949. } else {
  950. fatal
  951. "URL mismatch after rebase:",
  952. " $url_ != $expect_url";
  953. }
  954. }
  955. if ($uuid_ ne $uuid) {
  956. fatal "uuid mismatch after rebase: ",
  957. "$uuid_ != $uuid";
  958. }
  959. # remap parents
  960. my (%p, @l, $i);
  961. for ($i = 0; $i < scalar @$linear_refs; $i++) {
  962. my $new = $linear_refs_->[$i] or next;
  963. $p{$new} =
  964. $parents->{$linear_refs->[$i]};
  965. push @l, $new;
  966. }
  967. $parents = \%p;
  968. $linear_refs = \@l;
  969. undef $last_rev;
  970. }
  971. }
  972. }
  973. if ($old_head) {
  974. my $new_head = command_oneline(qw/rev-parse HEAD/);
  975. my $new_is_symbolic = eval {
  976. command_oneline(qw/symbolic-ref -q HEAD/);
  977. };
  978. if ($new_is_symbolic) {
  979. print "dcommitted the branch ", $head, "\n";
  980. } else {
  981. print "dcommitted on a detached HEAD because you gave ",
  982. "a revision argument.\n",
  983. "The rewritten commit is: ", $new_head, "\n";
  984. }
  985. command(['checkout', $old_head], STDERR => 0);
  986. }
  987. unlink $gs->{index};
  988. }
  989. sub cmd_branch {
  990. my ($branch_name, $head) = @_;
  991. unless (defined $branch_name && length $branch_name) {
  992. die(($_tag ? "tag" : "branch") . " name required\n");
  993. }
  994. $head ||= 'HEAD';
  995. my (undef, $rev, undef, $gs) = working_head_info($head);
  996. my $src = $gs->full_pushurl;
  997. my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
  998. my $allglobs = $remote->{ $_tag ? 'tags' : 'branches' };
  999. my $glob;
  1000. if ($#{$allglobs} == 0) {
  1001. $glob = $allglobs->[0];
  1002. } else {
  1003. unless(defined $_branch_dest) {
  1004. die "Multiple ",
  1005. $_tag ? "tag" : "branch",
  1006. " paths defined for Subversion repository.\n",
  1007. "You must specify where you want to create the ",
  1008. $_tag ? "tag" : "branch",
  1009. " with the --destination argument.\n";
  1010. }
  1011. foreach my $g (@{$allglobs}) {
  1012. my $re = Git::SVN::Editor::glob2pat($g->{path}->{left});
  1013. if ($_branch_dest =~ /$re/) {
  1014. $glob = $g;
  1015. last;
  1016. }
  1017. }
  1018. unless (defined $glob) {
  1019. my $dest_re = qr/\b\Q$_branch_dest\E\b/;
  1020. foreach my $g (@{$allglobs}) {
  1021. $g->{path}->{left} =~ /$dest_re/ or next;
  1022. if (defined $glob) {
  1023. die "Ambiguous destination: ",
  1024. $_branch_dest, "\nmatches both '",
  1025. $glob->{path}->{left}, "' and '",
  1026. $g->{path}->{left}, "'\n";
  1027. }
  1028. $glob = $g;
  1029. }
  1030. unless (defined $glob) {
  1031. die "Unknown ",
  1032. $_tag ? "tag" : "branch",
  1033. " destination $_branch_dest\n";
  1034. }
  1035. }
  1036. }
  1037. my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/};
  1038. my $url;
  1039. if (defined $_commit_url) {
  1040. $url = $_commit_url;
  1041. } else {
  1042. $url = eval { command_oneline('config', '--get',
  1043. "svn-remote.$gs->{repo_id}.commiturl") };
  1044. if (!$url) {
  1045. $url = $remote->{pushurl} || $remote->{url};
  1046. }
  1047. }
  1048. my $dst = join '/', $url, $lft, $branch_name, ($rgt || ());
  1049. if ($dst =~ /^https:/ && $src =~ /^http:/) {
  1050. $src=~s/^http:/https:/;
  1051. }
  1052. ::_req_svn();
  1053. my $ctx = SVN::Client->new(
  1054. auth => Git::SVN::Ra::_auth_providers(),
  1055. log_msg => sub {
  1056. ${ $_[0] } = defined $_message
  1057. ? $_message
  1058. : 'Create ' . ($_tag ? 'tag ' : 'branch ' )
  1059. . $branch_name;
  1060. },
  1061. );
  1062. eval {
  1063. $ctx->ls($dst, 'HEAD', 0);
  1064. } and die "branch ${branch_name} already exists\n";
  1065. print "Copying ${src} at r${rev} to ${dst}...\n";
  1066. $ctx->copy($src, $rev, $dst)
  1067. unless $_dry_run;
  1068. $gs->fetch_all;
  1069. }
  1070. sub cmd_find_rev {
  1071. my $revision_or_hash = shift or die "SVN or git revision required ",
  1072. "as a command-line argument\n";
  1073. my $result;
  1074. if ($revision_or_hash =~ /^r\d+$/) {
  1075. my $head = shift;
  1076. $head ||= 'HEAD';
  1077. my @refs;
  1078. my (undef, undef, $uuid, $gs) = working_head_info($head, \@refs);
  1079. unless ($gs) {
  1080. die "Unable to determine upstream SVN information from ",
  1081. "$head history\n";
  1082. }
  1083. my $desired_revision = substr($revision_or_hash, 1);
  1084. if ($_before) {
  1085. $result = $gs->find_rev_before($desired_revision, 1);
  1086. } elsif ($_after) {
  1087. $result = $gs->find_rev_after($desired_revision, 1);
  1088. } else {
  1089. $result = $gs->rev_map_get($desired_revision, $uuid);
  1090. }
  1091. } else {
  1092. my (undef, $rev, undef) = cmt_metadata($revision_or_hash);
  1093. $result = $rev;
  1094. }
  1095. print "$result\n" if $result;
  1096. }
  1097. sub auto_create_empty_directories {
  1098. my ($gs) = @_;
  1099. my $var = eval { command_oneline('config', '--get', '--bool',
  1100. "svn-remote.$gs->{repo_id}.automkdirs") };
  1101. # By default, create empty directories by consulting the unhandled log,
  1102. # but allow setting it to 'false' to skip it.
  1103. return !($var && $var eq 'false');
  1104. }
  1105. sub cmd_rebase {
  1106. command_noisy(qw/update-index --refresh/);
  1107. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1108. unless ($gs) {
  1109. die "Unable to determine upstream SVN information from ",
  1110. "working tree history\n";
  1111. }
  1112. if ($_dry_run) {
  1113. print "Remote Branch: " . $gs->refname . "\n";
  1114. print "SVN URL: " . $url . "\n";
  1115. return;
  1116. }
  1117. if (command(qw/diff-index HEAD --/)) {
  1118. print STDERR "Cannot rebase with uncommited changes:\n";
  1119. command_noisy('status');
  1120. exit 1;
  1121. }
  1122. unless ($_local) {
  1123. # rebase will checkout for us, so no need to do it explicitly
  1124. $_no_checkout = 'true';
  1125. $_fetch_all ? $gs->fetch_all : $gs->fetch;
  1126. }
  1127. command_noisy(rebase_cmd(), $gs->refname);
  1128. if (auto_create_empty_directories($gs)) {
  1129. $gs->mkemptydirs;
  1130. }
  1131. }
  1132. sub cmd_show_ignore {
  1133. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1134. $gs ||= Git::SVN->new;
  1135. my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
  1136. $gs->prop_walk($gs->path, $r, sub {
  1137. my ($gs, $path, $props) = @_;
  1138. print STDOUT "\n# $path\n";
  1139. my $s = $props->{'svn:ignore'} or return;
  1140. $s =~ s/[\r\n]+/\n/g;
  1141. $s =~ s/^\n+//;
  1142. chomp $s;
  1143. $s =~ s#^#$path#gm;
  1144. print STDOUT "$s\n";
  1145. });
  1146. }
  1147. sub cmd_show_externals {
  1148. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1149. $gs ||= Git::SVN->new;
  1150. my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
  1151. $gs->prop_walk($gs->path, $r, sub {
  1152. my ($gs, $path, $props) = @_;
  1153. print STDOUT "\n# $path\n";
  1154. my $s = $props->{'svn:externals'} or return;
  1155. $s =~ s/[\r\n]+/\n/g;
  1156. chomp $s;
  1157. $s =~ s#^#$path#gm;
  1158. print STDOUT "$s\n";
  1159. });
  1160. }
  1161. sub cmd_create_ignore {
  1162. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1163. $gs ||= Git::SVN->new;
  1164. my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
  1165. $gs->prop_walk($gs->path, $r, sub {
  1166. my ($gs, $path, $props) = @_;
  1167. # $path is of the form /path/to/dir/
  1168. $path = '.' . $path;
  1169. # SVN can have attributes on empty directories,
  1170. # which git won't track
  1171. mkpath([$path]) unless -d $path;
  1172. my $ignore = $path . '.gitignore';
  1173. my $s = $props->{'svn:ignore'} or return;
  1174. open(GITIGNORE, '>', $ignore)
  1175. or fatal("Failed to open `$ignore' for writing: $!");
  1176. $s =~ s/[\r\n]+/\n/g;
  1177. $s =~ s/^\n+//;
  1178. chomp $s;
  1179. # Prefix all patterns so that the ignore doesn't apply
  1180. # to sub-directories.
  1181. $s =~ s#^#/#gm;
  1182. print GITIGNORE "$s\n";
  1183. close(GITIGNORE)
  1184. or fatal("Failed to close `$ignore': $!");
  1185. command_noisy('add', '-f', $ignore);
  1186. });
  1187. }
  1188. sub cmd_mkdirs {
  1189. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1190. $gs ||= Git::SVN->new;
  1191. $gs->mkemptydirs($_revision);
  1192. }
  1193. # get_svnprops(PATH)
  1194. # ------------------
  1195. # Helper for cmd_propget and cmd_proplist below.
  1196. sub get_svnprops {
  1197. my $path = shift;
  1198. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1199. $gs ||= Git::SVN->new;
  1200. # prefix THE PATH by the sub-directory from which the user
  1201. # invoked us.
  1202. $path = $cmd_dir_prefix . $path;
  1203. fatal("No such file or directory: $path") unless -e $path;
  1204. my $is_dir = -d $path ? 1 : 0;
  1205. $path = join_paths($gs->path, $path);
  1206. # canonicalize the path (otherwise libsvn will abort or fail to
  1207. # find the file)
  1208. $path = canonicalize_path($path);
  1209. my $r = (defined $_revision ? $_revision : $gs->ra->get_latest_revnum);
  1210. my $props;
  1211. if ($is_dir) {
  1212. (undef, undef, $props) = $gs->ra->get_dir($path, $r);
  1213. }
  1214. else {
  1215. (undef, $props) = $gs->ra->get_file($path, $r, undef);
  1216. }
  1217. return $props;
  1218. }
  1219. # cmd_propget (PROP, PATH)
  1220. # ------------------------
  1221. # Print the SVN property PROP for PATH.
  1222. sub cmd_propget {
  1223. my ($prop, $path) = @_;
  1224. $path = '.' if not defined $path;
  1225. usage(1) if not defined $prop;
  1226. my $props = get_svnprops($path);
  1227. if (not defined $props->{$prop}) {
  1228. fatal("`$path' does not have a `$prop' SVN property.");
  1229. }
  1230. print $props->{$prop} . "\n";
  1231. }
  1232. # cmd_proplist (PATH)
  1233. # -------------------
  1234. # Print the list of SVN properties for PATH.
  1235. sub cmd_proplist {
  1236. my $path = shift;
  1237. $path = '.' if not defined $path;
  1238. my $props = get_svnprops($path);
  1239. print "Properties on '$path':\n";
  1240. foreach (sort keys %{$props}) {
  1241. print " $_\n";
  1242. }
  1243. }
  1244. sub cmd_multi_init {
  1245. my $url = shift;
  1246. unless (defined $_trunk || @_branches || @_tags) {
  1247. usage(1);
  1248. }
  1249. $_prefix = '' unless defined $_prefix;
  1250. if (defined $url) {
  1251. $url = canonicalize_url($url);
  1252. init_subdir(@_);
  1253. }
  1254. do_git_init_db();
  1255. if (defined $_trunk) {
  1256. $_trunk =~ s#^/+##;
  1257. my $trunk_ref = 'refs/remotes/' . $_prefix . 'trunk';
  1258. # try both old-style and new-style lookups:
  1259. my $gs_trunk = eval { Git::SVN->new($trunk_ref) };
  1260. unless ($gs_trunk) {
  1261. my ($trunk_url, $trunk_path) =
  1262. complete_svn_url($url, $_trunk);
  1263. $gs_trunk = Git::SVN->init($trunk_url, $trunk_path,
  1264. undef, $trunk_ref);
  1265. }
  1266. }
  1267. return unless @_branches || @_tags;
  1268. my $ra = $url ? Git::SVN::Ra->new($url) : undef;
  1269. foreach my $path (@_branches) {
  1270. complete_url_ls_init($ra, $path, '--branches/-b', $_prefix);
  1271. }
  1272. foreach my $path (@_tags) {
  1273. complete_url_ls_init($ra, $path, '--tags/-t', $_prefix.'tags/');
  1274. }
  1275. }
  1276. sub cmd_multi_fetch {
  1277. $Git::SVN::no_reuse_existing = undef;
  1278. my $remotes = Git::SVN::read_all_remotes();
  1279. foreach my $repo_id (sort keys %$remotes) {
  1280. if ($remotes->{$repo_id}->{url}) {
  1281. Git::SVN::fetch_all($repo_id, $remotes);
  1282. }
  1283. }
  1284. }
  1285. # this command is special because it requires no metadata
  1286. sub cmd_commit_diff {
  1287. my ($ta, $tb, $url) = @_;
  1288. my $usage = "Usage: $0 commit-diff -r<revision> ".
  1289. "<tree-ish> <tree-ish> [<URL>]";
  1290. fatal($usage) if (!defined $ta || !defined $tb);
  1291. my $svn_path = '';
  1292. if (!defined $url) {
  1293. my $gs = eval { Git::SVN->new };
  1294. if (!$gs) {
  1295. fatal("Needed URL or usable git-svn --id in ",
  1296. "the command-line\n", $usage);
  1297. }
  1298. $url = $gs->url;
  1299. $svn_path = $gs->path;
  1300. }
  1301. unless (defined $_revision) {
  1302. fatal("-r|--revision is a required argument\n", $usage);
  1303. }
  1304. if (defined $_message && defined $_file) {
  1305. fatal("Both --message/-m and --file/-F specified ",
  1306. "for the commit message.\n",
  1307. "I have no idea what you mean");
  1308. }
  1309. if (defined $_file) {
  1310. $_message = file_to_s($_file);
  1311. } else {
  1312. $_message ||= get_commit_entry($tb)->{log};
  1313. }
  1314. my $ra ||= Git::SVN::Ra->new($url);
  1315. my $r = $_revision;
  1316. if ($r eq 'HEAD') {
  1317. $r = $ra->get_latest_revnum;
  1318. } elsif ($r !~ /^\d+$/) {
  1319. die "revision argument: $r not understood by git-svn\n";
  1320. }
  1321. my %ed_opts = ( r => $r,
  1322. log => $_message,
  1323. ra => $ra,
  1324. tree_a => $ta,
  1325. tree_b => $tb,
  1326. editor_cb => sub { print "Committed r$_[0]\n" },
  1327. svn_path => $svn_path );
  1328. if (!Git::SVN::Editor->new(\%ed_opts)->apply_diff) {
  1329. print "No changes\n$ta == $tb\n";
  1330. }
  1331. }
  1332. sub cmd_info {
  1333. my $path = canonicalize_path(defined($_[0]) ? $_[0] : ".");
  1334. my $fullpath = canonicalize_path($cmd_dir_prefix . $path);
  1335. if (exists $_[1]) {
  1336. die "Too many arguments specified\n";
  1337. }
  1338. my ($file_type, $diff_status) = find_file_type_and_diff_status($path);
  1339. if (!$file_type && !$diff_status) {
  1340. print STDERR "svn: '$path' is not under version control\n";
  1341. exit 1;
  1342. }
  1343. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1344. unless ($gs) {
  1345. die "Unable to determine upstream SVN information from ",
  1346. "working tree history\n";
  1347. }
  1348. # canonicalize_path() will return "" to make libsvn 1.5.x happy,
  1349. $path = "." if $path eq "";
  1350. my $full_url = canonicalize_url( add_path_to_url( $url, $fullpath ) );
  1351. if ($_url) {
  1352. print "$full_url\n";
  1353. return;
  1354. }
  1355. my $result = "Path: $path\n";
  1356. $result .= "Name: " . basename($path) . "\n" if $file_type ne "dir";
  1357. $result .= "URL: $full_url\n";
  1358. eval {
  1359. my $repos_root = $gs->repos_root;
  1360. Git::SVN::remove_username($repos_root);
  1361. $result .= "Repository Root: " . canonicalize_url($repos_root) . "\n";
  1362. };
  1363. if ($@) {
  1364. $result .= "Repository Root: (offline)\n";
  1365. }
  1366. ::_req_svn();
  1367. $result .= "Repository UUID: $uuid\n" unless $diff_status eq "A" &&
  1368. (::compare_svn_version('1.5.4') <= 0 || $file_type ne "dir");
  1369. $result .= "Revision: " . ($diff_status eq "A" ? 0 : $rev) . "\n";
  1370. $result .= "Node Kind: " .
  1371. ($file_type eq "dir" ? "directory" : "file") . "\n";
  1372. my $schedule = $diff_status eq "A"
  1373. ? "add"
  1374. : ($diff_status eq "D" ? "delete" : "normal");
  1375. $result .= "Schedule: $schedule\n";
  1376. if ($diff_status eq "A") {
  1377. print $result, "\n";
  1378. return;
  1379. }
  1380. my ($lc_author, $lc_rev, $lc_date_utc);
  1381. my @args = Git::SVN::Log::git_svn_log_cmd($rev, $rev, "--", $fullpath);
  1382. my $log = command_output_pipe(@args);
  1383. my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
  1384. while (<$log>) {
  1385. if (/^${esc_color}author (.+) <[^>]+> (\d+) ([\-\+]?\d+)$/o) {
  1386. $lc_author = $1;
  1387. $lc_date_utc = Git::SVN::Log::parse_git_date($2, $3);
  1388. } elsif (/^${esc_color} (git-svn-id:.+)$/o) {
  1389. (undef, $lc_rev, undef) = ::extract_metadata($1);
  1390. }
  1391. }
  1392. close $log;
  1393. Git::SVN::Log::set_local_timezone();
  1394. $result .= "Last Changed Author: $lc_author\n";
  1395. $result .= "Last Changed Rev: $lc_rev\n";
  1396. $result .= "Last Changed Date: " .
  1397. Git::SVN::Log::format_svn_date($lc_date_utc) . "\n";
  1398. if ($file_type ne "dir") {
  1399. my $text_last_updated_date =
  1400. ($diff_status eq "D" ? $lc_date_utc : (stat $path)[9]);
  1401. $result .=
  1402. "Text Last Updated: " .
  1403. Git::SVN::Log::format_svn_date($text_last_updated_date) .
  1404. "\n";
  1405. my $checksum;
  1406. if ($diff_status eq "D") {
  1407. my ($fh, $ctx) =
  1408. command_output_pipe(qw(cat-file blob), "HEAD:$path");
  1409. if ($file_type eq "link") {
  1410. my $file_name = <$fh>;
  1411. $checksum = md5sum("link $file_name");
  1412. } else {
  1413. $checksum = md5sum($fh);
  1414. }
  1415. command_close_pipe($fh, $ctx);
  1416. } elsif ($file_type eq "link") {
  1417. my $file_name =
  1418. command(qw(cat-file blob), "HEAD:$path");
  1419. $checksum =
  1420. md5sum("link " . $file_name);
  1421. } else {
  1422. open FILE, "<", $path or die $!;
  1423. $checksum = md5sum(\*FILE);
  1424. close FILE or die $!;
  1425. }
  1426. $result .= "Checksum: " . $checksum . "\n";
  1427. }
  1428. print $result, "\n";
  1429. }
  1430. sub cmd_reset {
  1431. my $target = shift || $_revision or die "SVN revision required\n";
  1432. $target = $1 if $target =~ /^r(\d+)$/;
  1433. $target =~ /^\d+$/ or die "Numeric SVN revision expected\n";
  1434. my ($url, $rev, $uuid, $gs) = working_head_info('HEAD');
  1435. unless ($gs) {
  1436. die "Unable to determine upstream SVN information from ".
  1437. "history\n";
  1438. }
  1439. my ($r, $c) = $gs->find_rev_before($target, not $_fetch_parent);
  1440. die "Cannot find SVN revision $target\n" unless defined($c);
  1441. $gs->rev_map_set($r, $c, 'reset', $uuid);
  1442. print "r$r = $c ($gs->{ref_id})\n";
  1443. }
  1444. sub cmd_gc {
  1445. if (!can_compress()) {
  1446. warn "Compress::Zlib could not be found; unhandled.log " .
  1447. "files will not be compressed.\n";
  1448. }
  1449. find({ wanted => \&gc_directory, no_chdir => 1}, "$ENV{GIT_DIR}/svn");
  1450. }
  1451. ########################### utility functions #########################
  1452. sub rebase_cmd {
  1453. my @cmd = qw/rebase/;
  1454. push @cmd, '-v' if $_verbose;
  1455. push @cmd, qw/--merge/ if $_merge;
  1456. push @cmd, "--strategy=$_strategy" if $_strategy;
  1457. push @cmd, "--preserve-merges" if $_preserve_merges;
  1458. @cmd;
  1459. }
  1460. sub post_fetch_checkout {
  1461. return if $_no_checkout;
  1462. return if verify_ref('HEAD^0');
  1463. my $gs = $Git::SVN::_head or return;
  1464. # look for "trunk" ref if it exists
  1465. my $remote = Git::SVN::read_all_remotes()->{$gs->{repo_id}};
  1466. my $fetch = $remote->{fetch};
  1467. if ($fetch) {
  1468. foreach my $p (keys %$fetch) {
  1469. basename($fetch->{$p}) eq 'trunk' or next;
  1470. $gs = Git::SVN->new($fetch->{$p}, $gs->{repo_id}, $p);
  1471. last;
  1472. }
  1473. }
  1474. command_noisy(qw(update-ref HEAD), $gs->refname);
  1475. return unless verify_ref('HEAD^0');
  1476. return if $ENV{GIT_DIR} !~ m#^(?:.*/)?\.git$#;
  1477. my $index = $ENV{GIT_INDEX_FILE} || "$ENV{GIT_DIR}/index";
  1478. return if -f $index;
  1479. return if command_oneline(qw/rev-parse --is-inside-work-tree/) eq 'false';
  1480. return if command_oneline(qw/rev-parse --is-inside-git-dir/) eq 'true';
  1481. command_noisy(qw/read-tree -m -u -v HEAD HEAD/);
  1482. print STDERR "Checked out HEAD:\n ",
  1483. $gs->full_url, " r", $gs->last_rev, "\n";
  1484. if (auto_create_empty_directories($gs)) {
  1485. $gs->mkemptydirs($gs->last_rev);
  1486. }
  1487. }
  1488. sub complete_svn_url {
  1489. my ($url, $path) = @_;
  1490. $path = canonicalize_path($path);
  1491. # If the path is not a URL...
  1492. if ($path !~ m#^[a-z\+]+://#) {
  1493. if (!defined $url || $url !~ m#^[a-z\+]+://#) {
  1494. fatal("E: '$path' is not a complete URL ",
  1495. "and a separate URL is not specified");
  1496. }
  1497. return ($url, $path);
  1498. }
  1499. return ($path, '');
  1500. }
  1501. sub complete_url_ls_init {
  1502. my ($ra, $repo_path, $switch, $pfx) = @_;
  1503. unless ($repo_path) {
  1504. print STDERR "W: $switch not specified\n";
  1505. return;
  1506. }
  1507. $repo_path = canonicalize_path($repo_path);
  1508. if ($repo_path =~ m#^[a-z\+]+://#) {
  1509. $ra = Git::SVN::Ra->new($repo_path);
  1510. $repo_path = '';
  1511. } else {
  1512. $repo_path =~ s#^/+##;
  1513. unless ($ra) {
  1514. fatal("E: '$repo_path' is not a complete URL ",
  1515. "and a separate URL is not specified");
  1516. }
  1517. }
  1518. my $url = $ra->url;
  1519. my $gs = Git::SVN->init($url, undef, undef, undef, 1);
  1520. my $k = "svn-remote.$gs->{repo_id}.url";
  1521. my $orig_url = eval { command_oneline(qw/config --get/, $k) };
  1522. if ($orig_url && ($orig_url ne $gs->url)) {
  1523. die "$k already set: $orig_url\n",
  1524. "wanted to set to: $gs->url\n";
  1525. }
  1526. command_oneline('config', $k, $gs->url) unless $orig_url;
  1527. my $remote_path = join_paths( $gs->path, $repo_path );
  1528. $remote_path =~ s{%([0-9A-F]{2})}{chr hex($1)}ieg;
  1529. $remote_path =~ s#^/##g;
  1530. $remote_path .= "/*" if $remote_path !~ /\*/;
  1531. my ($n) = ($switch =~ /^--(\w+)/);
  1532. if (length $pfx && $pfx !~ m#/$#) {
  1533. die "--prefix='$pfx' must have a trailing slash '/'\n";
  1534. }
  1535. command_noisy('config',
  1536. '--add',
  1537. "svn-remote.$gs->{repo_id}.$n",
  1538. "$remote_path:refs/remotes/$pfx*" .
  1539. ('/*' x (($remote_path =~ tr/*/*/) - 1)) );
  1540. }
  1541. sub verify_ref {
  1542. my ($ref) = @_;
  1543. eval { command_oneline([ 'rev-parse', '--verify', $ref ],
  1544. { STDERR => 0 }); };
  1545. }
  1546. sub get_tree_from_treeish {
  1547. my ($treeish) = @_;
  1548. # $treeish can be a symbolic ref, too:
  1549. my $type = command_oneline(qw/cat-file -t/, $treeish);
  1550. my $expected;
  1551. while ($type eq 'tag') {
  1552. ($treeish, $type) = command(qw/cat-file tag/, $treeish);
  1553. }
  1554. if ($type eq 'commit') {
  1555. $expected = (grep /^tree /, command(qw/cat-file commit/,
  1556. $treeish))[0];
  1557. ($expected) = ($expected =~ /^tree ($sha1)$/o);
  1558. die "Unable to get tree from $treeish\n" unless $expected;
  1559. } elsif ($type eq 'tree') {
  1560. $expected = $treeish;
  1561. } else {
  1562. die "$treeish is a $type, expected tree, tag or commit\n";
  1563. }
  1564. return $expected;
  1565. }
  1566. sub get_commit_entry {
  1567. my ($treeish) = shift;
  1568. my %log_entry = (

Large files files are truncated, but you can click here to view the full file