[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

[PATCH] proposed v3 source format using .git.tar.gz



I've been working on making dpkg-source support a new source package format
based upon git. The idea is that a source package has only a .dsc and a
.git.tar.gz, which is just a git repo.

I've blogged[1] about some of what led me to this idea, and I've also written
a short FAQ[2]. Suggest reading both to understand where I'm coming from with
this.

[1] http://kitenet.net/~joey/blog/entry/an_evolutionary_change_to_the_Debian_source_package_format/
[2] http://wiki.debian.org/GitSrc

My implementation adds a new 3.0 version source format. A 3.0 format debian
source package can consist of any files allowed by formats 1 and 2, but
may also contain .$VCS.tar.gz files. To build a version 3 source package,
a new field is needed in debian/control:

Format: 3.0 (git)

The bit in parens specifies that it should use the git backend, which
is currently the only one available. That backend is in the
Dpkg::Source::VCS::git perl module.

I have a sourcev3 branch with my changes at <git://kitenet.net/dpkg>,
and have also attached a diff to this mail. I feel that this is ready
for review and hopefully merging into dpkg now. Looking forward to your
comments.

A sample dpkg source package built using this is at
<http://kitenet.net/~joey/tmp/git-demo/>. This demo package includes only
the last 200 commits to the dpkg git repo, so it's more than 1 mb *smaller*
than dpkg's normal .tar.gz!

-- 
see shy jo
diff --git a/debian/dpkg-dev.install b/debian/dpkg-dev.install
index 49e3835..ee65dbf 100644
--- a/debian/dpkg-dev.install
+++ b/debian/dpkg-dev.install
@@ -56,3 +56,4 @@ usr/share/man/*/dpkg-shlibdeps.1
 usr/share/man/*/*/dpkg-source.1
 usr/share/man/*/dpkg-source.1
 usr/share/perl5/Dpkg/BuildOptions.pm
+usr/share/perl5/Dpkg/Source
diff --git a/man/dpkg-source.1 b/man/dpkg-source.1
index 9bf9ff3..14c17c3 100644
--- a/man/dpkg-source.1
+++ b/man/dpkg-source.1
@@ -55,6 +55,10 @@ will look for the original source tarfile
 or the original source directory
 .IB directory .orig
 depending on the \fB\-sX\fP arguments.
+
+
+If the source package is being built as a version 3 source package using
+a VCS, no upstream tarball or original source directory is needed.
 .TP
 .BR \-h ", " \-\-help
 Show the usage message and exit.
@@ -109,7 +113,9 @@ This option negates a previously set
 .BR \-i [\fIregexp\fP]
 You may specify a perl regular expression to match files you want
 filtered out of the list of files for the diff. (This list is
-generated by a find command.) \fB\-i\fR by itself enables the option,
+generated by a find command.) (If the source package is being built as a
+version 3 source package using a VCS, this is instead used to
+ignore uncommitted files.) \fB\-i\fR by itself enables the option,
 with a default that will filter out control files and directories of the
 most common revision control systems, backup and swap files and Libtool
 build output directories. There can only be one active regexp, of multiple
@@ -162,6 +168,9 @@ will not overwrite existing tarfiles or directories. If this is
 desired then
 .BR \-sA ", " \-sP ", " \-sK ", " \-sU " and " \-sR
 should be used instead.
+.PP
+If the source package is being built as a version 3 source package using
+a VCS, these options do not make sense, and will be ignored.
 .TP
 .BR \-sk
 Specifies to expect the original source as a tarfile, by default
diff --git a/scripts/Dpkg/Source/VCS/git.pm b/scripts/Dpkg/Source/VCS/git.pm
new file mode 100644
index 0000000..cac7d05
--- /dev/null
+++ b/scripts/Dpkg/Source/VCS/git.pm
@@ -0,0 +1,226 @@
+#!/usr/bin/perl
+#
+# git support for dpkg-source
+#
+# Copyright © 2007 Joey Hess <joeyh@debian.org>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+package Dpkg::Source::VCS::git;
+
+use strict;
+use warnings;
+use Cwd;
+use File::Find;
+use Dpkg;
+use Dpkg::Gettext;
+
+push (@INC, $dpkglibdir);
+require 'controllib.pl';
+
+# Remove variables from the environment that might cause git to do
+# something unexpected.
+delete $ENV{GIT_DIR};
+delete $ENV{GIT_INDEX_FILE};
+delete $ENV{GIT_OBJECT_DIRECTORY};
+delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES};
+delete $ENV{GIT_WORK_TREE};
+
+sub sanity_check {
+	my $srcdir=shift;
+
+	if (! -s "$srcdir/.git") {
+		main::error(sprintf(_g("%s is not the top directory of a git repository (%s/.git not present), but Format git was specified"), $srcdir, $srcdir));
+	}
+	if (-s "$srcdir/.gitmodules") {
+		main::error(sprintf(_g("git repository %s uses submodules. This is not yet supported."), $srcdir));
+	}
+
+	# Symlinks from .git to outside could cause unpack failures, or
+	# point to files they shouldn't, so check for and don't allow.
+	if (-l "$srcdir/.git") {
+		main::error(sprintf(_g("%s is a symlink"), "$srcdir/.git"));
+	}
+	my $abs_srcdir=Cwd::abs_path($srcdir);
+	find(sub {
+		if (-l $_) {
+			if (Cwd::abs_path(readlink($_)) !~ /^\Q$abs_srcdir\E(\/|$)/) {
+				main::error(sprintf(_g("%s is a symlink to outside %s"), $File::Find::name, $srcdir));
+			}
+		}
+	}, "$srcdir/.git");
+
+}
+
+# Called before a tarball is created, to prepare the tar directory.
+sub prep_tar {
+	my $srcdir=shift;
+	my $tardir=shift;
+	
+	sanity_check($srcdir);
+
+	if (! -e "$srcdir/.git") {
+		main::error(sprintf(_g("%s is not a git repository, but Format git was specified"), $srcdir));
+	}
+	if (-e "$srcdir/.gitmodules") {
+		main::error(sprintf(_g("git repository %s uses submodules. This is not yet supported."), $srcdir));
+	}
+
+	# Check for uncommitted files.
+	open(GIT_STATUS, "LANG=C cd $srcdir && git-status |") ||
+		main::subprocerr("cd $srcdir && git-status");
+	my $clean=0;
+	my $status="";
+	while (<GIT_STATUS>) {
+		if (/^\Qnothing to commit (working directory clean)\E$/) {
+			$clean=1;
+		}
+		else {
+			$status.="git-status: $_";
+		}
+	}
+	close GIT_STATUS;
+	# git-status exits 1 if there are uncommitted changes or if
+	# the repo is clean, and 0 if there are uncommitted changes
+	# listed in the index.
+	if ($? && $? >> 8 != 1) {
+		main::subprocerr("cd $srcdir && git status");
+	}
+	if (! $clean) {
+		# To support dpkg-buildpackage -i, get a list of files
+		# eqivilant to the ones git-status finds, and remove any
+		# ignored files from it.
+		my @ignores="--exclude-per-directory=.gitignore";
+		my $core_excludesfile=`cd $srcdir && git-config --get core.excludesfile`;
+		chomp $core_excludesfile;
+		if (length $core_excludesfile && -e "$srcdir/$core_excludesfile") {
+			push @ignores, "--exclude-from='$core_excludesfile'";
+		}
+		if (-e "$srcdir/.git/info/exclude") {
+			push @ignores, "--exclude-from=.git/info/exclude";
+		}
+		open(GIT_LS_FILES, "cd $srcdir && git-ls-files -m -d -o @ignores |") ||
+			main::subprocerr("cd $srcdir && git-ls-files");
+		my @files;
+		while (<GIT_LS_FILES>) {
+			chomp;
+			if (! length $main::diff_ignore_regexp ||
+			    ! m/$main::diff_ignore_regexp/o) {
+				push @files, $_;
+			}
+		}
+		close(GIT_LS_FILES) || main::syserr("git-ls-files exited nonzero");
+
+		if (@files) {
+			print $status;
+			main::error(sprintf(_g("uncommitted, not-ignored changes in working directory: %s"),
+				join(" ", @files)));
+		}
+	}
+
+	# garbage collect the repo
+	system("cd $srcdir && git-gc --prune");
+	$? && main::subprocerr("cd $srcdir && git-gc --prune");
+
+	# TODO support for creating a shallow clone for those cases where
+	# uploading the whole repo history is not desired
+
+	mkdir($tardir,0755) ||
+            &syserr(sprintf(_g("unable to create `%s'"), $tardir));
+	system("cp -a $srcdir/.git $tardir");
+	$? && main::subprocerr("cp -a $srcdir/.git $tardir");
+
+	# As an optimisation, remove the index. It will be recreated by git
+	# reset during unpack. It's probably small, but you never know, this
+	# might save a lot of space.
+	unlink("$tardir/.git/index"); # error intentionally ignored
+}
+
+# Called after a tarball is unpacked, to check out the working copy.
+sub post_unpack_tar {
+	my $srcdir=shift;
+	
+	sanity_check($srcdir);
+
+	# Disable git hooks, as unpacking a source package should not
+	# involve running code.
+	foreach my $hook (glob("$srcdir/.git/hooks/*")) {
+		if (-x $hook) {
+			main::warning(sprintf(_g("executable bit set on %s; clearing"), $hook));
+			chmod(0644 &~ umask(), $hook) ||
+				main::syserr(sprintf(_g("unable to change permission of `%s'"), $hook));
+		}
+	}
+	
+	# This is a paranoia measure, since the index is not normally
+	# provided by possibly-untrusted third parties, remove it if
+	# present (git-rebase will recreate it).
+	if (-e "$srcdir/.git/index" || -l "$srcdir/.git/index") {
+		unlink("$srcdir/.git/index") ||
+			main::syserr(sprintf(_g("unable to remove `%s'"), "$srcdir/.git/index"));
+	}
+
+	# Comment out potentially probamatic or annoying stuff in
+	# .git/config.
+	my $safe_fields=qr/^(
+		core\.autocrlf			|
+		branch\..*			|
+		remote\..*			|
+		core\.repositoryformatversion	|
+		core\.filemode			|
+		core\.logallrefupdates		|
+		core\.bare
+		)$/x;
+	# This needs to be an absolute path, for some reason.
+	my $configfile=Cwd::abs_path("$srcdir/.git/config");
+	open(GIT_CONFIG, "git-config --file $configfile -l |") ||
+		main::subprocerr("git-config");
+	my @config=<GIT_CONFIG>;
+	close(GIT_CONFIG) || main::syserr("git-config exited nonzero");
+	my %removed_fields;
+	foreach (@config) {
+		chomp;
+		my ($field, $value)=split(/=/, $_, 2);
+		if ($field !~ /$safe_fields/) {
+			if (! exists $removed_fields{$field}) {
+				system("git-config", "--file", $configfile,
+					"--unset-all", $field);
+				$? && main::subprocerr("git-config --file $configfile --unset-all $field");
+			}
+			push @{$removed_fields{$field}}, $value;
+		}
+	}
+	if (%removed_fields) {
+		open(GIT_CONFIG, ">>", $configfile) ||
+			main::syserr(sprintf(_g("unstable to append to %s", $configfile)));
+		print GIT_CONFIG "\n# "._g("The following setting(s) were disabled by dpkg-source").":\n";
+		foreach my $field (sort keys %removed_fields) {
+			foreach my $value (@{$removed_fields{$field}}) {
+				print GIT_CONFIG "# $field=$value\n";
+			}
+		}
+		close GIT_CONFIG;
+		main::warning(_g(_g("modifying .git/config to comment out some settings")));
+	}
+
+	# Note that git-reset is used to repopulate the WC with files.
+	# git-clone isn't used because the repo might be an unclonable
+	# shallow copy. git-reset also recreates the index.
+	# XXX git-reset should be made to run in quiet mode here, but
+	# lacks a good way to do it. Bug filed.
+	system("cd $srcdir && git-reset --hard");
+	$? && main::subprocerr("cd $srcdir && git-reset --hard");
+}
+
+1
diff --git a/scripts/Makefile.am b/scripts/Makefile.am
index 87ea060..8238bbc 100644
--- a/scripts/Makefile.am
+++ b/scripts/Makefile.am
@@ -55,6 +55,7 @@ CLEANFILES = \
 
 perllibdir = $(PERL_LIBDIR)
 nobase_dist_perllib_DATA = \
+	Dpkg/Source/VCS/git.pm \
 	Dpkg/BuildOptions.pm \
 	Dpkg/Gettext.pm \
 	Dpkg.pm
diff --git a/scripts/dpkg-source.pl b/scripts/dpkg-source.pl
index 5b7802d..6c823c8 100755
--- a/scripts/dpkg-source.pl
+++ b/scripts/dpkg-source.pl
@@ -17,7 +17,7 @@ my $controlfile;
 my $changelogfile;
 my $changelogformat;
 
-my $diff_ignore_regexp = '';
+our $diff_ignore_regexp = '';
 my $diff_ignore_default_regexp = '
 # Ignore general backup files
 (?:^|/).*~$|
@@ -71,7 +71,7 @@ $diff_ignore_default_regexp =~ s/\n//sg;
 
 my $sourcestyle = 'X';
 my $min_dscformat = 1;
-my $max_dscformat = 2;
+my $max_dscformat = 3;
 my $def_dscformat = "1.0"; # default format for -b
 
 my $expectprefix;
@@ -190,6 +190,13 @@ sub handleformat {
 	return $1 >= $min_dscformat && $1 <= $max_dscformat;
 }
 
+sub loadvcs {
+	my $vcs = shift;
+	my $mod = "Dpkg::Source::VCS::$vcs";
+	eval qq{use $mod};
+	return ! $@;
+}
+
 
 my $opmode;
 my $tar_ignore_default_pattern_done;
@@ -252,10 +259,6 @@ $SIG{'PIPE'} = 'DEFAULT';
 
 if ($opmode eq 'build') {
 
-    $sourcestyle =~ y/X/A/;
-    $sourcestyle =~ m/[akpursnAKPUR]/ ||
-        &usageerr(sprintf(_g("source handling style -s%s not allowed with -b"), $sourcestyle));
-
     @ARGV || &usageerr(_g("-b needs a directory"));
     @ARGV<=2 || &usageerr(_g("-b takes at most a directory and an orig source argument"));
     my $dir = shift(@ARGV);
@@ -283,7 +286,7 @@ if ($opmode eq 'build') {
         if (s/^C //) {
 	    if (m/^Source$/i) {
 		setsourcepackage($v);
-	    } elsif (m/^(Standards-Version|Origin|Maintainer|Homepage)$/i ||
+	    } elsif (m/^(Format|Standards-Version|Origin|Maintainer|Homepage)$/i ||
 	             m/^Vcs-(Browser|Arch|Bzr|Cvs|Darcs|Git|Hg|Mtn|Svn)$/i) {
 		$f{$_}= $v;
 	    }
@@ -351,6 +354,39 @@ if ($opmode eq 'build') {
             &internerr(sprintf(_g("value from nowhere, with key >%s< and value >%s<"), $_, $v));
         }
     }
+    
+    my $vcs;
+    if ($f{Format} =~ /^\s*(\d+\.\d+)\s*$/) {
+	    if ($1 >= 3.0) {
+	        error(sprintf(_g("don't know how to generate %s format source package (missing vcs specifier in Format field?)"), $1));
+	    }
+	    if ($1 > 1.0) {
+	        error(sprintf(_g("don't know how to generate %s format source package"), $1));
+	    }
+    }
+    elsif ($f{Format} =~ /^\s*(\d+(?:\.\d+)?)\s+\((\w+)\)\s*$/) {
+	    $f{Format}=$1;
+	    if ($1 < 3.0) {
+	        error(sprintf(_g("control info file 'Format' field for version %s does not support vcs specifier \"%s\""), $1, $2));
+	    }
+            if ($1 >= 4) {
+	        error(sprintf(_g("unsupported control info file 'Format' value \"%s\""), $1));
+            }
+
+	    $vcs=$2;
+	    loadvcs($2)
+	    	|| error(sprintf(_g("unsupported vcs \"%s\" in control info file 'Format' field"), $2));
+            
+	    if ($sourcestyle =~ /[akpursKPUR]/) {
+		warning(sprintf(_g("source handling style -s%s not supported when generating %s format source package"), $sourcestyle, $vcs));
+            }
+            $sourcestyle='v';
+    }
+    
+    $sourcestyle =~ y/X/A/;
+    $sourcestyle =~ m/[akpursnAKPURv]/ ||
+        &usageerr(sprintf(_g("source handling style -s%s not allowed with -b"), $sourcestyle));
+
 
     $f{'Binary'}= join(', ',@binarypackages);
     for my $f (keys %override) {
@@ -438,7 +474,17 @@ if ($opmode eq 'build') {
     my $tardirbase;
     my $origdirname;
 
-    if ($sourcestyle ne 'n') {
+    if ($sourcestyle eq 'v') {
+	$tarname="$basenamerev.$vcs.tar.gz";
+        $tardirbase= $dirbase; $tardirname= "$tarname.tmp";
+
+	eval qq{Dpkg::Source::VCS::${vcs}::prep_tar(\$dir, \$tardirname)};
+	if ($@) {
+            &syserr($@);
+	}
+	push @exit_handlers, sub { erasedir($tardirname) };
+    }
+    elsif ($sourcestyle ne 'n') {
 	my $origdirbase = $origdir;
 	$origdirbase =~ s,/?$,,;
         $origdirbase =~ s,[^/]+$,,; $origdirname= $&;
@@ -458,10 +504,10 @@ if ($opmode eq 'build') {
         $tarname= "$basenamerev.tar.gz";
     }
 
-    if ($sourcestyle =~ m/[nurUR]/) {
+    if ($sourcestyle =~ m/[nurURv]/) {
 
         if (stat($tarname)) {
-            $sourcestyle =~ m/[nUR]/ ||
+            $sourcestyle =~ m/[nURv]/ ||
                 &error(sprintf(_g("tarfile `%s' already exists, not overwriting,".
                        " giving up; use -sU or -sR to override"), $tarname));
         } elsif ($! != ENOENT) {
@@ -489,7 +535,7 @@ if ($opmode eq 'build') {
             &syserr(sprintf(_g("unable to rename `%s' (newly created) to `%s'"), $newtar, $tarname));
 	chmod(0666 &~ umask(), $tarname) ||
 	    &syserr(sprintf(_g("unable to change permission of `%s'"), $tarname));
-
+	
     } else {
         
         printf(_g("%s: building %s using existing %s")."\n",
@@ -530,6 +576,10 @@ if ($opmode eq 'build') {
             &syserr(sprintf(_g("unable to remove `%s'"), "$origtargz.tmp-nest"));
 	    pop @exit_handlers;
     }
+
+    if ($sourcestyle eq 'v') {
+        erasedir($tardirname)
+    }
         
     if ($sourcestyle =~ m/[kpursKPUR]/) {
         
@@ -790,6 +840,7 @@ if ($opmode eq 'build') {
     my @tarfiles;
     my $difffile;
     my $debianfile;
+    my %vcsfiles;
     my %seen;
     for my $file (split(/\n /, $files)) {
         next if $file eq '';
@@ -813,6 +864,11 @@ if ($opmode eq 'build') {
 	    else    { unshift @tarfiles, $file; }
 	} elsif (/^\.debian\.tar$/) {
 	    $debianfile = $file;
+	} elsif (/^\.(\w+)\.tar$/) {
+            my $vcs=$1;
+            # TODO try to load vcs module
+            push @tarfiles, $file;
+            $vcsfiles{$file}=$vcs;
 	} elsif (/^\.diff$/) {
 	    $difffile = $file;
 	} else {
@@ -825,14 +881,17 @@ if ($opmode eq 'build') {
     if ($native) {
 	warning(_g("multiple tarfiles in native package")) if @tarfiles > 1;
 	warning(_g("native package with .orig.tar"))
-	    unless $seen{'.tar'} or $seen{"-$revision.tar"};
+	    unless $seen{'.tar'} or $seen{"-$revision.tar"} or %vcsfiles;
     } else {
-	warning(_g("no upstream tarfile in Files field")) unless $seen{'.orig.tar'};
+	warning(_g("no upstream tarfile in Files field")) unless $seen{'.orig.tar'} or %vcsfiles;
 	if ($dscformat =~ /^1\./) {
 	    warning(sprintf(_g("multiple upstream tarballs in %s format dsc"), $dscformat)) if @tarfiles > 1;
 	    warning(sprintf(_g("debian.tar in %s format dsc"), $dscformat)) if $debianfile;
 	}
     }
+    if (%vcsfiles && $dscformat !~ /^3\./) {
+	warning(sprintf(_g("<rc>.tar file in %s format dsc"), $dscformat));
+    }
 
     $newdirectory = $sourcepackage.'-'.$baseversion unless defined($newdirectory);
     $expectprefix = $newdirectory;
@@ -908,6 +967,16 @@ if ($opmode eq 'build') {
 		$? && subprocerr("cp $expectprefix to $newdirectory.tmp-keep");
 	    }
 	}
+
+        if (exists $vcsfiles{$tarfile}) {
+	    printf(_g("%s: extracting source from %s repository")."\n", $progname, $vcsfiles{$tarfile});
+	    loadvcs($vcsfiles{$tarfile})
+	    	|| error(sprintf(_g("unsupported vcs \"%s\""), $vcsfiles{$tarfile}));
+	    eval qq{Dpkg::Source::VCS::$vcsfiles{$tarfile}::post_unpack_tar(\$target)};
+	    if ($@) {
+                &syserr($@);
+	    }
+        }
     }
 
     my @patches;

Attachment: signature.asc
Description: Digital signature


Reply to: