git-if-needed: read patches from a series file.

Change-Id: I29c87e8448f89deaab0c04678296b25013982486
diff --git a/tools/git-if-needed/sh/git-if-needed b/tools/git-if-needed/sh/git-if-needed
index ec81f36..d2251fe 100755
--- a/tools/git-if-needed/sh/git-if-needed
+++ b/tools/git-if-needed/sh/git-if-needed
@@ -2,29 +2,104 @@
 
 set -e
 
+usage()
+{
+	cat <<EOUSAGE
+Usage:	git-if-needed command /path/to/file.patch
+	git-if-needed [-S] -s /path/to/series command
+
+Options:
+	-S	short paths: only use the last component of the patch path
+	-s	series file: apply all the patches in that file
+
+Examples:
+	git if-needed am /path/to/local.patch
+	git if-needed -s /path/to/many-patches/series am
+EOUSAGE
+}
+
+apply_single()
+{
+	local cmd="$1" fname="$2"
+
+	if [ ! -e "$fname" ] || [ ! -f "$fname" ] || [ ! -r "$fname" ]; then
+		echo "Not a readable regular file: $fname" 1>&2
+		exit 1
+	fi
+
+	local chgid="$(egrep -e '^[[:space:]]*Change-Id:[[:space:]]+I[0-9a-f]+$' -- "$fname" | awk '{ print $2 }')"
+	if ! expr "x$chgid" : 'xI[0-9a-f][0-9a-f]*$' > /dev/null; then
+		echo "Expected a single Change-Id line in $fname" 1>&2
+		exit 1
+	fi
+
+	if git --no-pager log | egrep -qe "^[[:space:]]+Change-Id:[[:space:]]+${chgid}\$"; then
+		echo "# Change $chgid already present in the Git history"
+	elif git --no-pager "$cmd" -- "$fname"; then
+		echo "# 'git $cmd $fname' succeeded"
+	else
+		echo "# 'git $cmd $fname' failed"
+		git --no-pager status || echo "# 'git status' also failed"
+		exit 1
+	fi
+}
+
+apply_series()
+{
+	local cmd="$1" series="$2" short="$3"
+
+	series="$(readlink -f -- "$series")"
+	series_dir="$(dirname -- "$series")"
+	curdir="$(readlink -f .)"
+
+	while read filename; do
+		dirname="$(dirname -- "$filename")"
+		if [ -n "$short" ]; then
+			dirname="$(basename -- "$dirname")"
+		fi
+
+		cd -- "$dirname"
+		apply_single "$cmd" "$series_dir/$filename"
+		cd -- "$curdir"
+	done < "$series"
+}
+
+unset series short
+while getopts 'Ss:' o; do
+	case "$o" in
+		S)
+			short=1
+			;;
+
+		s)
+			series="$OPTARG"
+			;;
+
+		*)
+			usage 1>&2
+			exit 1
+			;;
+	esac
+done
+
+shift "$((OPTIND - 1))"
+
+if [ -n "$series" ]; then
+	if [ "$#" -ne 1 ]; then
+		usage 1>&2
+		exit 1
+	fi
+
+	cmd="$1"
+	apply_series "$cmd" "$series" "$short"
+	exit 0
+fi
+
 if [ "$#" -ne 2 ]; then
-	echo 'Usage: git if-needed command filename' 1>&2
+	usage 1>&2
 	exit 1
 fi
 cmd="$1"
 fname="$2"
 
-if [ ! -e "$fname" ] || [ ! -f "$fname" ] || [ ! -r "$fname" ]; then
-	echo "Not a readable regular file: $fname" 1>&2
-	exit 1
-fi
-chgid="$(egrep -e '^[[:space:]]*Change-Id:[[:space:]]+I[0-9a-f]+$' -- "$fname" | awk '{ print $2 }')"
-if ! expr "x$chgid" : 'xI[0-9a-f][0-9a-f]*$' > /dev/null; then
-	echo "Expected a single Change-Id line in $fname" 1>&2
-	exit 1
-fi
-
-if git --no-pager log | egrep -qe "^[[:space:]]+Change-Id:[[:space:]]+${chgid}\$"; then
-	echo "# Change $chgid already present in the Git history"
-elif git --no-pager "$cmd" -- "$fname"; then
-	echo "# 'git $cmd $fname' succeeded"
-else
-	echo "# 'git $cmd $fname' failed"
-	git --no-pager status || echo "# 'git status' also failed"
-	exit 1
-fi
+apply_single "$cmd" "$fname"
diff --git a/tools/git-if-needed/tests/data/openstack/cinder/add-readme.patch b/tools/git-if-needed/tests/data/openstack/cinder/add-readme.patch
new file mode 100644
index 0000000..a6012ce
--- /dev/null
+++ b/tools/git-if-needed/tests/data/openstack/cinder/add-readme.patch
@@ -0,0 +1,21 @@
+From 8d0a6195ee106f7d18ff9f6d6d33747cf5db6250 Mon Sep 17 00:00:00 2001
+From: Peter Penchev <openstack-dev@storpool.com>
+Date: Tue, 3 Dec 2019 16:11:08 +0200
+Subject: [PATCH 1/3] Add a README file.
+
+Change-Id: I81a662baca9813772f6b1b83a1c034bd1fe6d53f
+---
+ README.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100644 README.txt
+
+diff --git a/README.txt b/README.txt
+new file mode 100644
+index 0000000..4dd1ef7
+--- /dev/null
++++ b/README.txt
+@@ -0,0 +1 @@
++This is a file.
+-- 
+2.24.0
+
diff --git a/tools/git-if-needed/tests/data/openstack/cinder/another-file.patch b/tools/git-if-needed/tests/data/openstack/cinder/another-file.patch
new file mode 100644
index 0000000..7286cdb
--- /dev/null
+++ b/tools/git-if-needed/tests/data/openstack/cinder/another-file.patch
@@ -0,0 +1,21 @@
+From 5c71ff4ba69bd0b95a6f3747886f10480dc24db3 Mon Sep 17 00:00:00 2001
+From: Peter Penchev <openstack-dev@storpool.com>
+Date: Tue, 3 Dec 2019 16:11:53 +0200
+Subject: [PATCH 3/3] Add another file.
+
+Change-Id: Ieb2a7d5346a74e8af3569d08057a8c83a537a1ad
+---
+ TODO.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100644 TODO.txt
+
+diff --git a/TODO.txt b/TODO.txt
+new file mode 100644
+index 0000000..209db4a
+--- /dev/null
++++ b/TODO.txt
+@@ -0,0 +1 @@
++Things to do. Or something.
+-- 
+2.24.0
+
diff --git a/tools/git-if-needed/tests/data/openstack/cinder/conflict-in-readme.patch b/tools/git-if-needed/tests/data/openstack/cinder/conflict-in-readme.patch
new file mode 100644
index 0000000..82039e5
--- /dev/null
+++ b/tools/git-if-needed/tests/data/openstack/cinder/conflict-in-readme.patch
@@ -0,0 +1,20 @@
+From 81dd71b0300a7819d8babf73072965cfc5d3a604 Mon Sep 17 00:00:00 2001
+From: Peter Penchev <openstack-dev@storpool.com>
+Date: Tue, 3 Dec 2019 16:12:25 +0200
+Subject: [PATCH 2/2] Modify README.txt in a different way.
+
+Change-Id: I2e216842094051a30d999bf8dfebf87810202c17
+---
+ README.txt | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/README.txt b/README.txt
+index 4dd1ef7..b38c628 100644
+--- a/README.txt
++++ b/README.txt
+@@ -1 +1 @@
+-This is a file.
++This is a file. Or something. I guess.
+-- 
+2.24.0
+
diff --git a/tools/git-if-needed/tests/data/openstack/cinder/modify-readme.patch b/tools/git-if-needed/tests/data/openstack/cinder/modify-readme.patch
new file mode 100644
index 0000000..4c75ba2
--- /dev/null
+++ b/tools/git-if-needed/tests/data/openstack/cinder/modify-readme.patch
@@ -0,0 +1,20 @@
+From 54a7cac37658e17af661b34b541507d50a24f0c9 Mon Sep 17 00:00:00 2001
+From: Peter Penchev <openstack-dev@storpool.com>
+Date: Tue, 3 Dec 2019 16:11:21 +0200
+Subject: [PATCH 2/3] Modify the README file.
+
+Change-Id: If062e5daee091fc17d3a2cacbbbaa39522385184
+---
+ README.txt | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/README.txt b/README.txt
+index 4dd1ef7..5e7d880 100644
+--- a/README.txt
++++ b/README.txt
+@@ -1 +1 @@
+-This is a file.
++This is a file. It contains some text.
+-- 
+2.24.0
+
diff --git a/tools/git-if-needed/tests/data/openstack/nova/add-readme.patch b/tools/git-if-needed/tests/data/openstack/nova/add-readme.patch
new file mode 100644
index 0000000..5c09fa1
--- /dev/null
+++ b/tools/git-if-needed/tests/data/openstack/nova/add-readme.patch
@@ -0,0 +1,21 @@
+From 5fdf1ec10ca56c7b58328aa1a544d9ec395bd95f Mon Sep 17 00:00:00 2001
+From: Peter Penchev <openstack-dev@storpool.com>
+Date: Tue, 3 Dec 2019 16:15:52 +0200
+Subject: [PATCH] Add a README file in another repository.
+
+Change-Id: I227fed25c0565b65ad46fa6ff4386a4547f9af45
+---
+ README.txt | 1 +
+ 1 file changed, 1 insertion(+)
+ create mode 100644 README.txt
+
+diff --git a/README.txt b/README.txt
+new file mode 100644
+index 0000000..d4e7da7
+--- /dev/null
++++ b/README.txt
+@@ -0,0 +1 @@
++This is another file. It has no bearing on the first one.
+-- 
+2.24.0
+
diff --git a/tools/git-if-needed/tests/data/series b/tools/git-if-needed/tests/data/series
new file mode 100644
index 0000000..fbb68d7
--- /dev/null
+++ b/tools/git-if-needed/tests/data/series
@@ -0,0 +1,4 @@
+openstack/cinder/add-readme.patch
+openstack/cinder/modify-readme.patch
+openstack/nova/add-readme.patch
+openstack/cinder/another-file.patch
diff --git a/tools/git-if-needed/tests/gifn-test.pl b/tools/git-if-needed/tests/gifn-test.pl
index 5de957d..989cbfa 100755
--- a/tools/git-if-needed/tests/gifn-test.pl
+++ b/tools/git-if-needed/tests/gifn-test.pl
@@ -346,6 +346,77 @@
 	}
 }
 
+sub setup_subdir_repos($)
+{
+	my ($cfg) = @_;
+
+	$cfg->{sub}->{full}->{base} = $cfg->{subrepo}->child('full');
+	$cfg->{sub}->{full}->{cinder} =
+	    $cfg->{sub}->{full}->{base}->
+	    child('openstack')->child('cinder');
+	$cfg->{sub}->{full}->{nova} =
+	    $cfg->{sub}->{full}->{base}->
+	    child('openstack')->child('nova');
+	$cfg->{sub}->{full}->{cinder}->mkpath({ mode => 0755 });
+	$cfg->{sub}->{full}->{nova}->mkpath({ mode => 0755 });
+
+	$cfg->{sub}->{short}->{base} = $cfg->{subrepo}->child('short');
+	$cfg->{sub}->{short}->{cinder} =
+	    $cfg->{sub}->{short}->{base}->child('cinder');
+	$cfg->{sub}->{short}->{nova} =
+	    $cfg->{sub}->{short}->{base}->child('nova');
+	$cfg->{sub}->{short}->{cinder}->mkpath({ mode => 0755 });
+	$cfg->{sub}->{short}->{nova}->mkpath({ mode => 0755 });
+
+	for my $part (qw(full short)) {
+		for my $comp (qw(cinder nova)) {
+			my $dir = $cfg->{sub}->{$part}->{$comp};
+			chdir($dir) or die "Could not change into $dir: $!\n";
+			run_command @{$cfg->{git}}, 'init';
+			git_status_ok $cfg;
+		}
+	}
+}
+
+sub test_subdir($ $ $ $)
+{
+	my ($cfg, $part, $expected, $opt) = @_;
+	my $run = $expected ? 'second' : 'first';
+	my $sub = $cfg->{sub}->{$part};
+
+	say "\ntest-subdir $part $run\n";
+
+	my $any = sub {
+	    $sub->{cinder}->child('README.txt')->exists ||
+	    $sub->{nova}->child('README.txt')->exists
+	};
+	my $all = sub {
+	    $sub->{cinder}->child('README.txt')->exists &&
+	    $sub->{nova}->child('README.txt')->exists
+	};
+
+	chdir $sub->{base} or die "Could not change into $sub->{base}: $!\n";
+	if ($expected && !$all->()) {
+		die "No $part files before the second run in $sub->{base}\n";
+	} elsif (!$expected && $any->()) {
+		die "Unexpected $part files in $sub->{base}\n";
+	}
+
+	run_command @{$cfg->{gifn}}, '-s', $cfg->{data}->child('series'),
+	    @{$opt}, 'am';
+
+	if (!$all->()) {
+		my $run = $expected ? 'second' : 'first';
+		die "No $part files after the $run run in $sub->{base}\n";
+	}
+
+	for my $comp (qw(cinder nova)) {
+		chdir $sub->{$comp} or
+		    die "Could not change into $sub->{comp}: $!\n";
+		git_status_ok $cfg;
+	}
+}
+
 MAIN:
 {
 	my %opts;
@@ -361,6 +432,9 @@
 	my $repodir = File::Temp->newdir(
 	    TEMPLATE => 'gifn-test.XXXXXX',
 	    TMPDIR => 1);
+	my $subrepodir = File::Temp->newdir(
+	    TEMPLATE => 'gifn-test.XXXXXX',
+	    TMPDIR => 1);
 
 	my $cfg = {
 		cwd => $cwd,
@@ -368,6 +442,7 @@
 		gifn => [@ARGV],
 		git => ['env', "LC_MESSAGES=$locale", 'git', '--no-pager'],
 		repo => path($repodir),
+		subrepo => path($subrepodir),
 	};
 
 	$cfg->{gifn}[0] = path($cfg->{gifn}[0])->absolute;
@@ -381,6 +456,13 @@
 		test_apply $cfg, @{PATCHFILES->{more}};
 		test_already_applied $cfg, (@{PATCHFILES->{initial}}, @{PATCHFILES->{more}});
 		test_fail_to_apply $cfg, @{PATCHFILES->{conflict}};
+
+		setup_subdir_repos $cfg;
+
+		test_subdir $cfg, 'full', 0, [];
+		test_subdir $cfg, 'short', 0, ['-S'];
+		test_subdir $cfg, 'full', 1, [];
+		test_subdir $cfg, 'short', 1, ['-S'];
 	};
 	my $err = $@;
 	chdir $cwd;