From 2b7ebdc147ac43af8178f21cfeb10a2e1855f8a9 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 7 Oct 2019 14:52:42 -0400 Subject: [PATCH 1/5] Fancy --sub_dir and --keep_hash This adds a fancy interaction between `--sub-dir` and `--keep_hash` that is meant to make sure we build *exactly* the right docs for pull requests. When we first deploy pull request builds we targeted them at the tip of the pull request branch. This caused trouble because the tip of the pull request branch was often behind the last successful build. Building the docs against that would often cause spurious failure from out of date links. Merging `master` into the PR branch usually fixed it but it was annoying. We fixed this problem by switching our target to the result of merging the pull request branch into the target branch. This was *better* because it had fewer spurious failures, but often caused us to think that the docs changes were larger than they actually were because the target branch often had changes in it that hadn't yet been picked up by the docs build. This was annoying because the `diff`s generated by the docs builds had changes that weren't part of the PR. Which made it difficult to track down what actually changed. This solves the problem by first merging the `--sub_dir`ed branch into the last successful docs build. Thus we only build *exactly* what is in the PR. We trigger this behavior by checking for both the `--keep_hash` and `--sub_dir` flags *and* checking for outstanding changes in the `--sub_dir`ed branch. This behavior might end up being slower sometimes because merging can be expensive, but it is usually less expensive than building a book we don't have to build. So maybe it'll all come out in the wash. --- build_docs.pl | 4 +- .../spec/all_books_change_detection_spec.rb | 7 +- integtest/spec/all_books_sub_dir_spec.rb | 210 ++++++++++++++++++ integtest/spec/helper/repo.rb | 7 + lib/ES/BaseRepo.pm | 13 ++ lib/ES/Repo.pm | 114 +++++++++- lib/ES/TargetRepo.pm | 21 +- 7 files changed, 342 insertions(+), 34 deletions(-) create mode 100644 integtest/spec/all_books_sub_dir_spec.rb diff --git a/build_docs.pl b/build_docs.pl index c743b4fa000d2..cc3333b8edda6 100755 --- a/build_docs.pl +++ b/build_docs.pl @@ -607,7 +607,7 @@ sub init_repos { tracker => $tracker, url => $url, reference => $reference_dir, - keep_hash => $Opts->{keep_hash}, + keep_hash => $Opts->{keep_hash} || 0, ); delete $child_dirs{ $repo->git_dir->absolute }; @@ -644,7 +644,7 @@ sub init_repos { ES::DocsRepo->new( tracker => $tracker, dir => $conf->{docs} || '/docs_build', - keep_hash => $Opts->{keep_hash} + keep_hash => $Opts->{keep_hash} || 0 ); return $tracker; diff --git a/integtest/spec/all_books_change_detection_spec.rb b/integtest/spec/all_books_change_detection_spec.rb index 70952e607782e..936876572891f 100644 --- a/integtest/spec/all_books_change_detection_spec.rb +++ b/integtest/spec/all_books_change_detection_spec.rb @@ -401,6 +401,7 @@ def chapter(index) before_second_build: lambda do |src, config| repo = src.repo 'repo' repo.write 'dummy', 'dummy' + repo.commit 'dummy' config.extra do |conversion| conversion.keep_hash.sub_dir(repo, 'master') @@ -619,7 +620,9 @@ def chapter(index) book = src.book 'Test' book.source repo2, 'not_used_actually' repo = src.repo 'repo' + repo.switch_to_newbranch 'subbed' repo.write 'index.asciidoc', TWO_CHAPTERS + "\nmore words" + repo.commit 'sub' config.extra do |conversion| conversion.keep_hash.sub_dir(repo, 'master') end @@ -722,9 +725,9 @@ def self.add_branch(src) context 'the second build' do let(:out) { outputs[1] } include_examples 'commits changes' - it "doesn't print that it is building the original branch" do + it "doesn't print that it is building any branch" do # The original book hasn't changed so we don't rebuild it - expect(out).not_to include('Test: Building master...') + expect(out).not_to include('Test: Building') end end end diff --git a/integtest/spec/all_books_sub_dir_spec.rb b/integtest/spec/all_books_sub_dir_spec.rb new file mode 100644 index 0000000000000..b9763cfa65f6f --- /dev/null +++ b/integtest/spec/all_books_sub_dir_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +RSpec.describe 'building all books' do + describe '--sub_dir' do + ## + # Setups up a repo that looks like: + # master sub_me + # original master-------->subbed + # | + # new master + # | + # too new master + # + # Optionally runs the build once on the commit "new master". Then it always + # runs the build, substituting sub_me for the master branch. If: + # * --keep_hash isn't specified or + # * the sub_me branch has outstanding changes or + # * there was a merge conflict or + # * the build wasn't run against "new master" + # then --sub_dir will pick up the contents of the directory and the build + # won't have "new maser" because it was forked from "original master". + # Otherwise, --keep_hash and --sub_dir will cause the build to merge + # "new master" and "subbed" and build against *that*. + def self.convert_with_sub(keep_hash: true, commit_sub: true, + build_with_init: true, + cause_merge_conflict: false, premerge: false) + convert_before do |src, dest| + repo = setup_repo src + setup_book src, repo + dest.prepare_convert_all(src.conf).convert if build_with_init + modify_master_after_build repo + setup_sub repo, commit_sub, cause_merge_conflict, premerge + convert src, repo, dest, keep_hash + end + end + + def self.setup_repo(src) + repo = src.repo 'repo' + repo.write 'docs/index.adoc', index + repo.write 'docs/from_master.adoc', 'original master' + repo.write 'docs/from_subbed.adoc', 'unsubbed' + repo.commit 'original master' + repo.write 'docs/from_master.adoc', 'new master' + repo.write 'docs/conflict', 'from master' + repo.commit 'new master' + repo + end + + def self.setup_book(src, repo) + book = src.book 'Test' + book.index = 'docs/index.adoc' + book.source repo, 'docs' + end + + def self.modify_master_after_build(repo) + repo.write 'docs/from_master.adoc', 'too new master' + repo.commit 'too new master' + end + + def self.setup_sub(repo, commit_sub, cause_merge_conflict, premerge) + repo.switch_to_branch 'HEAD~2' + repo.switch_to_new_branch 'sub_me' + repo.write 'docs/from_subbed.adoc', 'now subbed' + repo.write 'docs/conflict', 'from subbed' if cause_merge_conflict + repo.commit 'subbed' if commit_sub + repo.merge 'master' if premerge + end + + def self.convert(src, repo, dest, keep_hash) + builder = dest.prepare_convert_all src.conf + builder.sub_dir repo, 'master' + builder.keep_hash if keep_hash + builder.convert + dest.checkout_conversion + end + + def self.index + <<~ASCIIDOC + = Title + + [[chapter]] + == Chapter + + include::from_master.adoc[] + include::from_subbed.adoc[] + ASCIIDOC + end + + let(:logs) { outputs[-1] } + + shared_examples 'examples' do |master| + file_context 'raw/test/master/chapter.html' do + it "contains the #{master} master changes" do + expect(contents).to include("

#{master} master

") + end + it 'contains the subbed changes' do + expect(contents).to include('

now subbed

') + end + end + end + shared_examples 'contains the original master and subbed changes' do + include_examples 'examples', 'original' + end + shared_examples 'contains the new master and subbed changes' do + include_examples 'examples', 'new' + end + shared_examples 'contains the too new master and subbed changes' do + include_examples 'examples', 'too new' + end + shared_examples 'log merge' do |path| + it 'log that it started merging' do + expect(logs).to include(<<~LOGS) + Test: Merging the subbed dir for [repo][master][#{path}] into the last successful build. + LOGS + end + it 'log that it merged' do + expect(logs).to include(<<~LOGS) + Test: Merged the subbed dir for [repo][master][#{path}] into the last successful build. + LOGS + end + end + + describe 'without --keep_hash' do + convert_with_sub keep_hash: false + it "doesn't log that it won't merge because of uncommitted changes" do + expect(logs).not_to include(<<~LOGS) + Test: Not merging the subbed dir for [repo][master][docs] because it has uncommitted changes. + LOGS + end + include_examples 'contains the original master and subbed changes' + end + describe 'with --keep_hash' do + describe 'when there are uncommitted changes' do + convert_with_sub commit_sub: false + it "logs that it won't merge because of uncommitted changes" do + expect(logs).to include(<<~LOGS) + Test: Not merging the subbed dir for [repo][master][docs] because it has uncommitted changes. + LOGS + end + include_examples 'contains the original master and subbed changes' + end + describe 'when the source is new' do + convert_with_sub build_with_init: false + it "log that it won't merge because the source is new" do + expect(logs).to include(<<~LOGS) + Test: Not merging the subbed dir for [repo][master][docs] because it is new. + LOGS + end + include_examples 'contains the original master and subbed changes' + end + describe 'when the subbed dir can be merged' do + convert_with_sub + include_examples 'log merge', 'docs' + include_examples 'contains the new master and subbed changes' + end + describe 'when the subbed dir has already been merged' do + # This simulates what github will do if you ask it to build the "sha" + # of the merged PR instead of the "head" of the branch. + convert_with_sub premerge: true + include_examples 'log merge', 'docs' + include_examples 'contains the too new master and subbed changes' + end + describe 'when there is a conflict merging the subbed dir' do + convert_with_sub cause_merge_conflict: true + it 'logs that it failed to merge' do + expect(logs).to include(<<~LOGS) + Test: Failed to merge the subbed dir for [repo][master][docs] into the last successful build: + LOGS + end + it 'logs the conflict' do + expect(logs).to include(<<~LOGS) + CONFLICT (add/add): Merge conflict in docs/conflict + LOGS + end + include_examples 'contains the original master and subbed changes' + end + describe 'when there is more than one source using the same repo' do + def self.setup_book(src, repo) + book = src.book 'Test' + book.index = 'docs/index.adoc' + book.source repo, 'docs/index.adoc' + book.source repo, 'docs/from_master.adoc' + book.source repo, 'docs/from_subbed.adoc' + end + convert_with_sub + include_examples 'log merge', 'docs/index.adoc' + include_examples 'log merge', 'docs/from_master.adoc' + include_examples 'log merge', 'docs/from_subbed.adoc' + include_examples 'contains the new master and subbed changes' + end + describe 'when more than one book uses the same source' do + def self.setup_book(src, repo) + %w[Test Test2].each do |name| + book = src.book name + book.index = 'docs/index.adoc' + book.source repo, 'docs' + end + end + convert_with_sub + include_examples 'log merge', 'docs' + it 'logs only one merge' do + # This asserts that we log a single merge. We *should* be using the + # cache instead. + expect(logs).not_to match(/Merged.+Merged/m) + end + include_examples 'contains the new master and subbed changes' + end + end + end +end diff --git a/integtest/spec/helper/repo.rb b/integtest/spec/helper/repo.rb index c08ec707ea36d..00bd5c33f778e 100644 --- a/integtest/spec/helper/repo.rb +++ b/integtest/spec/helper/repo.rb @@ -93,6 +93,7 @@ def switch_to_new_branch(new_branch) ## # Checks out a branch. def switch_to_branch(branch) + # TODO: rename to checkout? Dir.chdir @root do sh "git checkout #{branch}" end @@ -139,6 +140,12 @@ def push_to(dest) end end + def merge(ref) + Dir.chdir @root do + sh "git merge #{ref}" + end + end + def copy_shared_conf FileUtils.mkdir_p @root sh "cp -r /docs_build/shared #{@root}" diff --git a/lib/ES/BaseRepo.pm b/lib/ES/BaseRepo.pm index f123975ccbed4..bda0c3c9559b5 100644 --- a/lib/ES/BaseRepo.pm +++ b/lib/ES/BaseRepo.pm @@ -141,6 +141,19 @@ sub _reference_args { return (); } +#=================================== +# Write a sparse checkout config for the repo. +#=================================== +sub _write_sparse_config { +#=================================== + my ( $self, $root, $config ) = @_; + + my $dest = $root->subdir( '.git' )->subdir( 'info' )->file( 'sparse-checkout' ); + open(my $sparse, '>', $dest) or dir("Couldn't write sparse config"); + print $sparse $config; + close $sparse; +} + #=================================== sub name { shift->{name} } sub git_dir { shift->{git_dir} } diff --git a/lib/ES/Repo.pm b/lib/ES/Repo.pm index b741f0cff932b..32a5da0392b9c 100644 --- a/lib/ES/Repo.pm +++ b/lib/ES/Repo.pm @@ -4,9 +4,10 @@ use strict; use warnings; use v5.10; -use Path::Class(); +use Cwd; use Encode qw(decode_utf8); use ES::Util qw(run); +use Path::Class(); use parent qw( ES::BaseRepo ); @@ -139,8 +140,7 @@ sub prepare { my $dest = $dest_root->subdir( $prefix ); if ( exists $self->{sub_dirs}->{$branch} ) { - my $source_root = $self->{sub_dirs}->{$branch}; - $self->_extract_from_dir( $source_root, $dest, $path ); + $self->_prepare_sub_dir( $title, $branch, $path, $dest ); return $dest; } @@ -151,17 +151,94 @@ sub prepare { return $dest; } - local $ENV{GIT_DIR} = $self->git_dir; + $self->_extract_from_ref( $dest, $branch, $path ); + return $dest; +} - $dest->mkpath; - my $tar = $dest->file('.temp_git_archive.tar'); - die "File <$tar> already exists" if -e $tar; - run qw(git archive --format=tar -o ), $tar, $resolved_branch, $path; +#=================================== +sub _prepare_sub_dir { +#=================================== + my ( $self, $title, $branch, $path, $dest ) = @_; + my $source_root = $self->{sub_dirs}->{$branch}; - run "tar", "-x", "-C", $dest, "-f", $tar; - $tar->remove; + unless ( $self->{keep_hash} ) { + $self->_extract_from_dir( $source_root, $dest, $path ); + return $dest; + } - return $dest; + my $has_uncommitted_changes = eval { + local $ENV{GIT_WORK_TREE} = $source_root; + local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git'; + run qw(git diff-index --quiet HEAD --); + 1; + }; + unless ( $has_uncommitted_changes ) { + printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it has uncommitted changes.\n", + $title, $self->{name}, $branch, $path); + $self->_extract_from_dir( $source_root, $dest, $path ); + return $dest; + } + + my $resolved_branch = $self->_resolve_branch( $title, $branch, $path ); + unless ( $resolved_branch ) { + printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it is new.\n", + $title, $self->{name}, $branch, $path); + $self->_extract_from_dir( $source_root, $dest, $path ); + return $dest; + } + + local $ENV{GIT_DIR} = "$source_root/.git"; + my $subbed_head = run qw(git rev-parse HEAD); + delete local $ENV{GIT_DIR}; + my $merge_branch = "${resolved_branch}_${subbed_head}_$path"; + $merge_branch =~ s|/|_|g; + + # Check if we've already merged this path by looking for the merged_branch + # in the source repo. This is safe because: + # 1. We prune all branches from the source repo before the build. + # 2. The merge branch contains the hash of the subbed head. + my $already_built = eval { + local $ENV{GIT_DIR} = $self->{git_dir}; + run qw(git rev-parse), $merge_branch; + 1; + }; + if ( $already_built ) { + # Logging here would be pretty noisy. + $self->_extract_from_ref( $dest, $merge_branch, $path ); + return; + } + + # Merge the HEAD of the subbed dir into the commit that last successfully + # built the docs. + printf(" - %40.40s: Merging the subbed dir for [%s][%s][%s] into the last successful build.\n", + $title, $self->{name}, $branch, $path); + my $work = Path::Class::dir( '/tmp/mergework' ); + $work->rmtree; + run qw(git clone --no-checkout), $self->{git_dir}, $work; + my $original_pwd = Cwd::cwd(); + chdir $work; + my $merged = eval { + run qw(git remote add subbed_repo), $source_root; + run qw(git fetch subbed_repo), $subbed_head; + run qw(git remote remove subbed_repo); + run qw(git config core.sparseCheckout true); + $self->_write_sparse_config( $work, $path ); + run qw(git checkout -b), $merge_branch, $resolved_branch; + run qw(git merge -m merge), $subbed_head; + run qw(git push origin -f), $merge_branch; # Origin here is just our clone. + 1; + }; + chdir $original_pwd; + unless ( $merged ) { + printf(" - %40.40s: Failed to merge the subbed dir for [%s][%s][%s] into the last successful build:\n%s", + $title, $self->{name}, $branch, $path, $@); + $dest->rmtree; + $self->_extract_from_dir( $source_root, $dest, $path ); + return $dest; + } + printf(" - %40.40s: Merged the subbed dir for [%s][%s][%s] into the last successful build.\n", + $title, $self->{name}, $branch, $path); + $self->_extract_from_ref( $dest, $merge_branch, $path ); } #=================================== @@ -184,6 +261,21 @@ sub _extract_from_dir { } or die "Error copying from $source: $@"; } +#=================================== +sub _extract_from_ref { +#=================================== + my ( $self, $dest, $ref, $path ) = @_; + local $ENV{GIT_DIR} = $self->{git_dir}; + + $dest->mkpath; + my $tar = $dest->file( '.temp_git_archive.tar' ); + die "File <$tar> already exists" if -e $tar; + run qw(git archive --format=tar -o), $tar, $ref, $path; + + run qw(tar -x -C), $dest, '-f', $tar; + $tar->remove; +} + #=================================== sub _tracker_branch { #=================================== diff --git a/lib/ES/TargetRepo.pm b/lib/ES/TargetRepo.pm index b4a62e4f139eb..c1514ab00967c 100644 --- a/lib/ES/TargetRepo.pm +++ b/lib/ES/TargetRepo.pm @@ -68,7 +68,7 @@ sub checkout_minimal { $self->{started_empty} = 0; chdir $self->{destination}; run qw(git config core.sparseCheckout true); - $self->_write_sparse_config("/*\n!html/*/\n!raw/*/"); + $self->_write_sparse_config( $self->{destination}, "/*\n!html/*/\n!raw/*/" ); run qw(git checkout master); return 1 if $self->{branch} eq 'master'; if ( $self->_branch_exists( 'origin/' . $self->{branch} ) ) { @@ -95,7 +95,7 @@ sub checkout_all { my $original_pwd = Cwd::cwd(); chdir $self->{destination}; eval { - $self->_write_sparse_config("*\n"); + $self->_write_sparse_config( $self->{destination}, "*\n" ); run qw(git read-tree -mu HEAD) unless $self->{started_empty}; 1; } or die "Error checking out repo : $@"; @@ -152,23 +152,6 @@ sub push_changes { run @push_branch; } -#=================================== -# Write a sparse checkout config for the repo. -#=================================== -sub _write_sparse_config { -#=================================== - my ( $self, $config ) = @_; - - open(my $sparse, '>', - $self->{destination} - ->subdir( '.git' ) - ->subdir( 'info' ) - ->file( 'sparse-checkout' )) - or dir("Couldn't write sparse config"); - print $sparse $config; - close $sparse; -} - #=================================== # Does a branch exist? #=================================== From c98de99e1075b8bff9947d5b9bc40fd859e49853 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 7 Oct 2019 17:29:12 -0400 Subject: [PATCH 2/5] explain --- README.asciidoc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 562e8a51f8b62..9f620ffed14f0 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -231,13 +231,21 @@ To check links before you merge your changes: into the master branch of the `elasticsearch` repo, run: + ---------------------------- -./docs/build_docs --all --target_repo git@github.com:elastic/built-docs.git --open --sub_dir elasticsearch:master:./elasticsearch +./docs/build_docs --all --target_repo git@github.com:elastic/built-docs.git \ + --open --keep_hash --sub_dir elasticsearch:master:./elasticsearch ---------------------------- -To run a full build to mimic the website build, omit the `--sub_dir` option: +NOTE: If there are no outstanding changes in the `../elasticsearch` directory + then this will build against the result of merging the last successful + docs build and the contents of `../elasticsearch`. If there *are* + outstanding changes then it'll just build against the contents of + `../elasticsearch`. + +To run a full build to mimic the website build, omit the `--sub_dir` and +`--keep_hash` options: ---------------------------- -build_docs --all --target_repo git@github.com:elastic/built-docs.git --open +./build_docs --all --target_repo git@github.com:elastic/built-docs.git --open ---------------------------- The first time you run a full build is slow as it needs to: From 44d037422ce16308d753ec28547976a3b8f9ffcc Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 8 Oct 2019 08:18:49 -0400 Subject: [PATCH 3/5] speeling --- integtest/Makefile | 2 +- integtest/spec/all_books_change_detection_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integtest/Makefile b/integtest/Makefile index a81f3fc49c1bb..c2ac6bd7ea8a6 100644 --- a/integtest/Makefile +++ b/integtest/Makefile @@ -17,4 +17,4 @@ rubocop: .PHONY: rspec rspec: - rspec + rspec -e'because we add a source to the book and specify --keep_hash and --sub_dir' diff --git a/integtest/spec/all_books_change_detection_spec.rb b/integtest/spec/all_books_change_detection_spec.rb index 936876572891f..9e819b16beedd 100644 --- a/integtest/spec/all_books_change_detection_spec.rb +++ b/integtest/spec/all_books_change_detection_spec.rb @@ -620,7 +620,7 @@ def chapter(index) book = src.book 'Test' book.source repo2, 'not_used_actually' repo = src.repo 'repo' - repo.switch_to_newbranch 'subbed' + repo.switch_to_new_branch 'subbed' repo.write 'index.asciidoc', TWO_CHAPTERS + "\nmore words" repo.commit 'sub' config.extra do |conversion| From c44916655f1547c92e8dee0475b064a6919a3ab8 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 8 Oct 2019 08:32:30 -0400 Subject: [PATCH 4/5] revert accident --- integtest/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integtest/Makefile b/integtest/Makefile index c2ac6bd7ea8a6..a81f3fc49c1bb 100644 --- a/integtest/Makefile +++ b/integtest/Makefile @@ -17,4 +17,4 @@ rubocop: .PHONY: rspec rspec: - rspec -e'because we add a source to the book and specify --keep_hash and --sub_dir' + rspec From fb74f55f030759043ab4368e011c6d1bd1d0d18d Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 8 Oct 2019 15:43:59 -0400 Subject: [PATCH 5/5] Fixup --- integtest/spec/all_books_sub_dir_spec.rb | 4 ++-- lib/ES/Repo.pm | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integtest/spec/all_books_sub_dir_spec.rb b/integtest/spec/all_books_sub_dir_spec.rb index b9763cfa65f6f..97bec59e904ab 100644 --- a/integtest/spec/all_books_sub_dir_spec.rb +++ b/integtest/spec/all_books_sub_dir_spec.rb @@ -108,12 +108,12 @@ def self.index include_examples 'examples', 'too new' end shared_examples 'log merge' do |path| - it 'log that it started merging' do + it "log that it started merging [#{path}]" do expect(logs).to include(<<~LOGS) Test: Merging the subbed dir for [repo][master][#{path}] into the last successful build. LOGS end - it 'log that it merged' do + it "log that it merged [#{path}]" do expect(logs).to include(<<~LOGS) Test: Merged the subbed dir for [repo][master][#{path}] into the last successful build. LOGS diff --git a/lib/ES/Repo.pm b/lib/ES/Repo.pm index 32a5da0392b9c..545365faaba7a 100644 --- a/lib/ES/Repo.pm +++ b/lib/ES/Repo.pm @@ -166,13 +166,13 @@ sub _prepare_sub_dir { return $dest; } - my $has_uncommitted_changes = eval { + my $no_uncommitted_changes = eval { local $ENV{GIT_WORK_TREE} = $source_root; local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git'; run qw(git diff-index --quiet HEAD --); 1; }; - unless ( $has_uncommitted_changes ) { + unless ( $no_uncommitted_changes ) { printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it has uncommitted changes.\n", $title, $self->{name}, $branch, $path); $self->_extract_from_dir( $source_root, $dest, $path );