summaryrefslogtreecommitdiff
path: root/doc/plugins/rawhtml
ModeNameSize
-rw-r--r--discussion.mdwn413logplain
span class="hl kwd">qr/[0-9a-fA-F]{40}/; # pattern to validate Git sha1sums
  • my $dummy_commit_msg = 'dummy commit'; # message to skip in recent changes
  • sub _safe_git (&@) { #{{{
  • # Start a child process safely without resorting /bin/sh.
  • # Return command output or success state (in scalar context).
  • my ($error_handler, @cmdline) = @_;
  • my $pid = open my $OUT, "-|";
  • error("Cannot fork: $!") if !defined $pid;
  • if (!$pid) {
  • # In child.
  • # Git commands want to be in wc.
  • chdir $config{srcdir}
  • or error("Cannot chdir to $config{srcdir}: $!");
  • exec @cmdline or error("Cannot exec '@cmdline': $!");
  • }
  • # In parent.
  • my @lines;
  • while (<$OUT>) {
  • chomp;
  • push @lines, $_;
  • }
  • close $OUT;
  • $error_handler->("'@cmdline' failed: $!") if $? && $error_handler;
  • return wantarray ? @lines : ($? == 0);
  • }
  • # Convenient wrappers.
  • sub run_or_die ($@) { _safe_git(\&error, @_) }
  • sub run_or_cry ($@) { _safe_git(sub { warn @_ }, @_) }
  • sub run_or_non ($@) { _safe_git(undef, @_) }
  • #}}}
  • sub _merge_past ($$$) { #{{{
  • # Unlike with Subversion, Git cannot make a 'svn merge -rN:M file'.
  • # Git merge commands work with the committed changes, except in the
  • # implicit case of '-m' of git checkout(1). So we should invent a
  • # kludge here. In principle, we need to create a throw-away branch
  • # in preparing for the merge itself. Since branches are cheap (and
  • # branching is fast), this shouldn't cost high.
  • #
  • # The main problem is the presence of _uncommitted_ local changes. One
  • # possible approach to get rid of this situation could be that we first
  • # make a temporary commit in the master branch and later restore the
  • # initial state (this is possible since Git has the ability to undo a
  • # commit, i.e. 'git reset --soft HEAD^'). The method can be summarized
  • # as follows:
  • #
  • # - create a diff of HEAD:current-sha1
  • # - dummy commit
  • # - create a dummy branch and switch to it
  • # - rewind to past (reset --hard to the current-sha1)
  • # - apply the diff and commit
  • # - switch to master and do the merge with the dummy branch
  • # - make a soft reset (undo the last commit of master)
  • #
  • # The above method has some drawbacks: (1) it needs a redundant commit
  • # just to get rid of local changes, (2) somewhat slow because of the
  • # required system forks. Until someone points a more straight method
  • # (which I would be grateful) I have implemented an alternative method.
  • # In this approach, we hide all the modified files from Git by renaming
  • # them (using the 'rename' builtin) and later restore those files in
  • # the throw-away branch (that is, we put the files themselves instead
  • # of applying a patch).
  • my ($sha1, $file, $message) = @_;
  • my @undo; # undo stack for cleanup in case of an error
  • my $conflict; # file content with conflict markers
  • eval {
  • # Hide local changes from Git by renaming the modified file.
  • # Relative paths must be converted to absolute for renaming.
  • my ($target, $hidden) = (
  • "$config{srcdir}/${file}", "$config{srcdir}/${file}.${sha1}"
  • );
  • rename($target, $hidden)
  • or error("rename '$target' to '$hidden' failed: $!");
  • # Ensure to restore the renamed file on error.
  • push @undo, sub {
  • return if ! -e "$hidden"; # already renamed
  • rename($hidden, $target)
  • or warn "rename '$hidden' to '$target' failed: $!";
  • };
  • my $branch = "throw_away_${sha1}"; # supposed to be unique
  • # Create a throw-away branch and rewind backward.
  • push @undo, sub { run_or_cry('git', 'branch', '-D', $branch) };
  • run_or_die('git', 'branch', $branch, $sha1);
  • # Switch to throw-away branch for the merge operation.
  • push @undo, sub {
  • if (!run_or_cry('git', 'checkout', $config{gitmaster_branch})) {
  • run_or_cry('git', 'checkout','-f',$config{gitmaster_branch});
  • }
  • };
  • run_or_die('git', 'checkout', $branch);
  • # Put the modified file in _this_ branch.
  • rename($hidden, $target)
  • or error("rename '$hidden' to '$target' failed: $!");
  • # _Silently_ commit all modifications in the current branch.
  • run_or_non('git', 'commit', '-m', $message, '-a');
  • # ... and re-switch to master.
  • run_or_die('git', 'checkout', $config{gitmaster_branch});
  • # Attempt to merge without complaining.
  • if (!run_or_non('git', 'pull', '--no-commit', '.', $branch)) {
  • $conflict = readfile($target);
  • run_or_die('git', 'reset', '--hard');
  • }
  • };
  • my $failure = $@;
  • # Process undo stack (in reverse order). By policy cleanup
  • # actions should normally print a warning on failure.
  • while (my $handle = pop @undo) {
  • $handle->();
  • }
  • error("Git merge failed!\n$failure\n") if $failure;
  • return $conflict;
  • } #}}}
  • sub _parse_diff_tree ($@) { #{{{
  • # Parse the raw diff tree chunk and return the info hash.
  • # See git-diff-tree(1) for the syntax.
  • my ($prefix, $dt_ref) = @_;
  • # End of stream?
  • return if !defined @{ $dt_ref } ||
  • !defined @{ $dt_ref }[0] || !length @{ $dt_ref }[0];
  • my %ci;
  • # Header line.
  • while (my $line = shift @{ $dt_ref }) {