diff --git a/setup/cli/modules/deploy.php b/setup/cli/modules/deploy.php
index 7d0e87ecc21b86cb596e2b20067028a9f8ba7171..c0d8ca8417de228c61b2ead32514bb49975128c9 100644
--- a/setup/cli/modules/deploy.php
+++ b/setup/cli/modules/deploy.php
@@ -87,12 +87,26 @@ class Deployment extends Unpacker {
         }
     }
 
-    function copyFile($src, $dest) {
+    function writeManifest($root) {
+        $lines = array();
+        foreach ($this->manifest as $F=>$H)
+            $lines[] = "$H $F";
+
+        return file_put_contents($root.'/.MANIFEST', implode("\n", $lines));
+    }
+
+    function hashContents($file) {
+        $md5 = md5($file);
+        $sha1 = sha1($file);
+        return substr($md5, -20) . substr($sha1, -20);
+    }
+
+    function getEditedContents($src) {
         static $short = false;
         static $version = false;
 
         if (substr($src, -4) != '.php')
-            return parent::copyFile($src, $dest);
+            return false;
 
         if (!$short) {
             $hash = exec('git rev-parse HEAD');
@@ -103,7 +117,7 @@ class Deployment extends Unpacker {
             $version = exec('git describe');
 
         if (!$short || !$version)
-            return parent::copyFile($src, $dest);
+            return false;
 
         $source = file_get_contents($src);
         $source = preg_replace(':<script(.*) src="([^"]+)\.js"></script>:',
@@ -125,12 +139,64 @@ class Deployment extends Unpacker {
             "$1ini_set('$2', '0'); // Set by installer",
             $source);
 
-        if (!file_put_contents($dest, $source))
-            die("Unable to apply rewrite rules to ".$dest);
+        return $source;
+    }
+
+    function copyFile($source, $dest, $hash=false) {
+        $contents = $this->getEditedContents($source);
+        if ($contents === false)
+            // Regular file
+            return parent::copyFile($source, $dest, $hash);
+
+        if (!file_put_contents($dest, $contents))
+            $this->fail($dest.": Unable to apply rewrite rules");
 
         return true;
     }
 
+    function unpackage($folder, $destination, $recurse=0, $exclude=false) {
+        return parent::unpackage($folder, $destination, $recurse, $exclude);
+
+        // TODO: Consider using `git ls-files` for deployment
+        if (substr($destination, -1) !== '/')
+            $destination .= '/';
+        $source = $this->source;
+        if (substr($source, -1) != '/')
+            $source .= '/';
+
+        $pipes = array();
+        $patterns = array();
+        foreach ((array) $exclude as $x) {
+            $patterns[] = str_replace($source, '', $x);
+        }
+        $exclude = implode(' --exclude-per-directory=', $patterns);
+        if (!($files = proc_open(
+            "git ls-files -s --exclude-standard --exclude-per-directory=$exclude $folder",
+            array(1 => array('pipe', 'w')),
+            $pipes
+        ))) {
+            return parent::unpackage($folder, $destination, $recurse, $exclude);
+        }
+
+        $dryrun = $this->getOption('dry-run', false);
+        $verbose = $this->getOption('verbose') || $dryrun;
+        while ($line = stream_get_line($pipes[1], 255, PHP_EOL)) {
+            list($mode, $hash, , $path) = preg_split('/\s+/', $line);
+            $src = $source.$path;
+            $dst = $destination.$path;
+            if (!$this->isChanged($src, false, $hash))
+                continue;
+            if ($verbose)
+                $this->stdout->write($dst."\n");
+            if ($dryrun)
+                continue;
+            if (!is_dir(dirname($dst)))
+                mkdir(dirname($dst), 0751, true);
+            // TODO: Consider the MODE value
+            $this->copyFile($src, $dst, $hash);
+        }
+    }
+
     function run($args, $options) {
         $this->destination = $args['install-path'];
         if (!is_dir($this->destination))
@@ -150,9 +216,12 @@ class Deployment extends Unpacker {
         $include = rtrim($include, '/').'/';
 
         # Locate the upload folder
-        $root = $this->find_root_folder();
+        $root = $this->source = $this->find_root_folder();
         $rootPattern = str_replace("\\","\\\\", $root); //need for windows case
 
+        # Prime the manifest system
+        $this->readManifest($this->destination.'/.MANIFEST');
+
         $exclusions = array("$rootPattern/include", "$rootPattern/.git*",
             "*.sw[a-z]","*.md", "*.txt");
         if (!$options['setup'])
@@ -177,6 +246,8 @@ class Deployment extends Unpacker {
                 array("ost-config.php","settings.php","plugins/",
                 "*/.htaccess"));
         }
+
+        $this->writeManifest($this->destination);
     }
 }
 
diff --git a/setup/cli/modules/unpack.php b/setup/cli/modules/unpack.php
index ecc6823d85bcd00b9314d477c61d1b434795617f..749b71d8999df47a1b1be3fae4b1159595f12e18 100644
--- a/setup/cli/modules/unpack.php
+++ b/setup/cli/modules/unpack.php
@@ -34,6 +34,10 @@ class Unpacker extends Module {
              main installation path.",
     );
 
+    var $manifest;
+    var $source;
+    var $destination;
+
     function realpath($path) {
         return ($p = realpath($path)) ? $p : $path;
     }
@@ -88,7 +92,51 @@ class Unpacker extends Module {
         return false;
     }
 
-    function copyFile($src, $dest) {
+    function readManifest($file) {
+        if (isset($this->manifest))
+            return @$this->manifest[$file] ?: null;
+
+        $this->manifest = $lines = array();
+        $path = $this->destination . '/.MANIFEST';
+        if (!is_file($path))
+            return null;
+
+        if (!preg_match_all('/^(\w+) (.+)$/mu', file_get_contents($path),
+            $lines, PREG_PATTERN_ORDER)
+        ) {
+            return null;
+        }
+
+        $this->manifest = array_combine($lines[2], $lines[1]);
+        return @$this->manifest[$file] ?: null;
+    }
+
+    function hashFile($file) {
+        static $hashes = array();
+
+        if (!isset($hashes[$file])) {
+            $md5 = md5_file($file);
+            $sha1 = sha1_file($file);
+            $hash = substr($md5, -20) . substr($sha1, -20);
+            $hashes[$file] = $hash;
+        }
+        return $hashes[$file];
+    }
+
+    function isChanged($source, $hash=false) {
+        $local = str_replace($this->source.'/', '', $source);
+        $hash = $hash ?: $this->hashFile($source);
+        return $this->readManifest($local) != $hash;
+    }
+
+    function updateManifest($file, $hash=false) {
+        $hash = $hash ?: $this->hashFile($file);
+        $local = str_replace($this->source.'/', '', $file);
+        $this->manifest[$local] = $hash;
+    }
+
+    function copyFile($src, $dest, $hash=false) {
+        $this->updateManifest($src, $hash);
         return copy($src, $dest);
     }
 
@@ -116,15 +164,17 @@ class Unpacker extends Module {
             if ($this->exclude($exclude, $file))
                 continue;
             if (is_file($file)) {
-                if (!is_dir($destination) && !$dryrun)
-                    mkdir($destination, 0751, true);
                 $target = $destination . basename($file);
-                if (is_file($target) && (md5_file($target) == md5_file($file)))
+                $hash = $this->hashFile($file);
+                if (is_file($target) && !$this->isChanged($file, $hash))
                     continue;
                 if ($verbose)
                     $this->stdout->write($target."\n");
-                if (!$dryrun)
-                    $this->copyFile($file, $target);
+                if ($dryrun)
+                    continue;
+                if (!is_dir($destination))
+                    mkdir($destination, 0751, true);
+                $this->copyFile($file, $target);
             }
         }
         if ($recurse) {
@@ -169,7 +219,7 @@ class Unpacker extends Module {
         $upgrade = file_exists("{$this->destination}/main.inc.php");
 
         # Locate the upload folder
-        $upload = $this->find_upload_folder();
+        $upload = $this->source = $this->find_upload_folder();
 
         # Unpack the upload folder to the destination, except the include folder
         if ($upgrade)