96

I would like to get a list of modified and added files in an specific commit so that I can export them and generate a package with the file structure.

The idea is to get the package and extract it on the server. For many reasons I can't create a hook to pull the repo automatically and the easiest way I have to keep the server updated is generating this package.

random
  • 9,774
  • 10
  • 66
  • 83
Michael Kuhinica
  • 1,148
  • 2
  • 11
  • 15

14 Answers14

132
git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id
  1. git diff-tree -r $commit_id:

    Take a diff of the given commit to its parent(s) (including all subdirectories, not just the top directory).

  2. --no-commit-id --name-only:

    Do not output the commit SHA1. Output only the names of the affected files instead of a full diff.

  3. --diff-filter=ACMRT:

    Only show files added, copied, modified, renamed or that had their type changed (eg. file → symlink) in this commit. This leaves out deleted files.


UPDATE FROM THE COMMENT:
Base on the question context and the comments below, with the following command, you can get the ACMRT files as a .tar file with their folder structure.

git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | tar -czf file.tgz -T -
Mohi
  • 1,776
  • 1
  • 26
  • 39
Aristotle Pagaltzis
  • 112,955
  • 23
  • 98
  • 97
  • Now I just have to find ou a way to glue the result of this command with tar! Thank you! – Michael Kuhinica Jan 04 '11 at 21:58
  • 9
    @Taverneiro You can pipe it to the [tar command given in this answer](http://stackoverflow.com/questions/2597875/how-can-i-build-a-tar-from-stdin), eg. for a tgz file : `git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | tar -czf file.tgz -T -` – Matthieu Sadouni Dec 19 '14 at 10:09
  • 2
    You can create a zip-file through git archive by: git archive -o upadate.zip HEAD $(git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT ) – Nathan Rona Mar 28 '22 at 09:44
  • BTW the [doc for `--diff-filter=ACMRT` is here](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203), so you can e.g. also exclude modified and added files (as per the question title) by using lower-case letters: `--diff-filter=am` – rklec Jul 27 '23 at 12:45
70
git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | xargs tar -rf mytarfile.tar

Just to round this out, here is the command piped to tar. This exports the files into a tar archive.

James Ehly
  • 1,231
  • 9
  • 14
  • 1
    Can this be done for multiple commits into one tar file? – evolutionxbox Aug 14 '14 at 13:54
  • 4
    When trying to add multiple commits to the same tar file, sometimes tar creates filename.ext again with other name (filename1.ext). I realize that if I use zip, I can add and overwrite the file if it exists in the zip... You just need to replace the xargs with xargs zip -r0 myzipfile.zip $1 – Ricardo Martins Sep 23 '14 at 17:39
  • Is xargs needed here? I know it is for stuff like rm, but I thought tar could handle as many files as you want. – Erdal G. Jun 07 '16 at 08:18
  • 1
    Be carefull with this answer, this take diff files but as the current version of the current branch – mems Mar 20 '18 at 16:15
  • 1
    While executing this command in the windows slave machine, I'm getting the following error "'xargs' is not recognized as an internal or external command, operable program or batch file." – Navin prasad Jan 23 '19 at 13:06
  • @mems assume the diff is fileA, then how to get the $commit_id version of fileA instead of the current version of fileA? – user1169587 Jun 28 '19 at 16:59
43

Here is a one-line command that works on Windows 7. Run it from your repository's top-level folder.

for /f "usebackq tokens=*" %A in (`git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT HEAD~1 HEAD`) do echo FA|xcopy "%~fA" "C:\git_changed_files\%A"

  • echo FA answers the inevitable xcopy question about whether you're copying a file or a directory (file), and the possible question about overwriting a file (overwrite All)
  • usebackq allows us to use the output from our git command as the input to our do clause
  • HEAD~1 HEAD gets all differences between the previous commit and the current HEAD
  • %~fA transforms the git output into fully qualified paths (needed to change forward slashes to backslashes)
  • C:\git_changed_files\ is where you will find all the files that are different
MM.
  • 1,966
  • 4
  • 20
  • 24
  • 1
    When I saw that mess of symbols I did not expect it to work without extensive tweaking... but it worked on the first try, thanks. – Nathan Fig Mar 27 '14 at 20:33
  • 3
    a little heads up for all the guys who are (like me) not very familiar with windows batch files: If you want to use the above command in a batch file, you will need to replace %A with %%A inside the for loop – buddybubble Nov 17 '15 at 09:20
  • 1
    What? LOL. Just use the *git archive - git diff* combo (Nicolas' answer). Much easier and straightforward. *(I honestly don't know what's happening here.)* – ADTC Nov 23 '15 at 03:01
  • 1
    Warning: this will copy files from your working copy, which may not actually match what's in HEAD (or whatever commit hash you replace it with) – Simon East Mar 07 '19 at 03:17
  • 1
    Great stuff, but works with only CMD not Powershell, -v°v- – Rodion Bykov Jun 30 '20 at 12:23
39

if your commit hash is for example a9359f9, this command :

git archive -o patch.zip a9359f9 $(git diff --name-only a9359f9^..a9359f9)

will extract the files modified in the commit and place them in patch.zip while keeping the project directory structure intact.

a bit verbose, the commit hash is mentioned three times, but it seems to work for me.

got it here : http://tosbourn.com/2011/05/git/using-git-to-create-an-archive-of-changed-files/

Nicolas Dermine
  • 628
  • 5
  • 8
  • I think I can use `function` in *PowerShell* to reduce the command to just a function name and use `$args` to pass in the commit hash just once. In *nix, you can probably use shell-scripting to achieve the same effect. – ADTC Aug 28 '14 at 04:10
  • 2
    Man... I own you a beer :) – deanpodgornik Apr 02 '15 at 18:41
  • just a note that git archive doesn't work for a range of revisions – Ben Sewards Jun 12 '15 at 19:11
  • 4
    @BenSewards for range of commits between commit1 and commit2 *(inclusive of later commit, exclusive of earlier commit; order doesn't matter)* just do `git archive -o patch.zip HEAD /**or a commit**/ $(git diff --name-only commit1 commit2)`. The commit passed to the `archive` command can be any arbitrary commit *(most likely either the HEAD or the later commit)*, and the files are pulled as if they are checked out at that commit stage. The *commit1* and *commit2* passed to `diff` are only used to generate the list of files to pull - they do not affect the version of files pulled. – ADTC Nov 23 '15 at 02:56
  • 1
    **Additional tip:** If you want to combine the changed files of multiple ranges of commits into one archive, just repeat the `diff` command with a semicolon: `git archive -o patch.zip HEAD /**or a commit**/ $(git diff --name-only commit1A commit1B; git diff --name-only commit2A commit2B; git diff --name-only commit3A commit3B)` – ADTC Nov 23 '15 at 03:28
  • 4
    Due to spaces in path names I had to use a pipe to xargs handling the NUL `git diff -z --name-only commit1 commit2 | xargs -0 git archive -o patch.zip HEAD` – Boggin Jun 02 '16 at 10:03
  • @ADTC cannot use git archive -o patch.zip HEAD /**or a commit**/ $(git diff --name-only commit1A..commit3B)? but need to repat the diff command with a semicolon? – user1169587 Jun 28 '19 at 17:56
  • @user1169587 your version is for a single range of commits (ALL commits from 1A to 3B). I was talking about combining multiple ranges of commits (1st range 1A to 1B; 2nd range 2A to 2B; 3rd range 3A to 3B; etc) so the commits outside these ranges would be ignored. – ADTC Aug 11 '19 at 10:54
35

You can export diff using Tortoise Git to MS Windows:

I right click and select TortoiseGit > Show Log and Log Messages will be open.

Select two revisions and compare it. Difference between will be open.

Select the files and Export selection to ... to a folder!

enter image description here

Wendel
  • 2,809
  • 29
  • 28
  • 1
    Definitely. So many answers to this had linux-only commands, and a lot did not capture working changes, only changes which had been committed. Glad this was posted! – dythim Jul 17 '19 at 18:33
  • Not sure if this is something new, but you can do this straight from the "Show Log" window. Simply highlight all the files and right-click with "Export selection to...". – Wasted_Coder Jun 15 '23 at 20:42
8

I needed to update my test server and add files that changed since version 2.1.
For me worked similar solution as James Ehly posted, but in my case i wanted to export to archive package of difference between two older tags - tag_ver_2.1 and tag_ver_2.2 not the only one commit.

For example:

tag_ver_2.1 = 1f72b38ad
tag_ver_2.2 = c1a546782

Here is modified example:

git diff-tree -r --no-commit-id --name-only c1a546782 1f72b38ad | xargs tar -rf test.tar
pietr
  • 141
  • 1
  • 6
  • I've just detected a WEIRD issue: if I try the code above it works like a charm. But I change the tar -rf test.tar for tar -zcf test.tar.gz, then the resulting file is missing several files. What can be causing this difference in result? – Alberto Alexander Rodriguez Jun 19 '17 at 16:40
  • While executing this command in the windows slave machine, I'm getting the following error "'xargs' is not recognized as an internal or external command, operable program or batch file." – Navin prasad Jan 23 '19 at 12:48
7

Below commands worked for me.

If you want difference of the files changed by the last commit:

git archive -o update.zip HEAD $(git diff --name-only HEAD HEAD^)

or if you want difference between two specific commits:

git archive -o update.zip sha1 $(git diff --name-only sha1 sha2)

or if you have uncommitted files, remember git way is to commit everything, branches are cheap:

git stash
git checkout -b feature/new-feature
git stash apply
git add --all
git commit -m 'commit message here'
git archive -o update.zip HEAD $(git diff --name-only HEAD HEAD^)
me_astr
  • 922
  • 1
  • 14
  • 21
3

I have made a php script to export changed files on Windows. If you have a localhost development server with php set up then you can run it easily. It will remember your last repository and export always to the same folder. The export folder is always emptied before exporting. You will also see deleted files in red so you know what to delete on the server.

These are just two files so I will post them here. Let's assume your repositories are located under c:/www in their own folders and that http://localhost also points to c:/www and is php-enabled. Let's put these 2 files in c:/www/git-export -

index.php :

<?php
/* create directory if doesn't exist */
function createDir($dirName, $perm = 0777) {
    $dirs = explode('/', $dirName);
    $dir='';
    foreach ($dirs as $part) {
        $dir.=$part.'/';
        if (!is_dir($dir) && strlen($dir)>0) {
            mkdir($dir, $perm);
        }
    }
}

/* deletes dir recursevely, be careful! */
function deleteDirRecursive($f) {

    if (strpos($f, "c:/www/export" . "/") !== 0) {
        exit("deleteDirRecursive() protection disabled deleting of tree: $f  - please edit the path check in source php file!");
    }

    if (is_dir($f)) {
        foreach(scandir($f) as $item) {
            if ($item == '.' || $item == '..') {
                continue;
            }

            deleteDirRecursive($f . "/" . $item);
        }    
        rmdir($f);

    } elseif (is_file($f)) {
        unlink($f);
    }
}

$lastRepoDirFile = "last_repo_dir.txt";
$repo = isset($_POST['repo']) ? $_POST['repo'] : null;


if (!$repo && is_file($lastRepoDirFile)) {
    $repo = file_get_contents($lastRepoDirFile);
}

$range = isset($_POST['range']) ? $_POST['range'] : "HEAD~1 HEAD";


$ini = parse_ini_file("git-export.ini");

$exportDir = $ini['export_dir'];
?>

<html>
<head>
<title>Git export changed files</title>
</head>

<body>
<form action="." method="post">
    repository: <?=$ini['base_repo_dir'] ?>/<input type="text" name="repo" value="<?=htmlspecialchars($repo) ?>" size="25"><br/><br/>

    range: <input type="text" name="range" value="<?=htmlspecialchars($range) ?>" size="100"><br/><br/>

    target: <strong><?=$exportDir ?></strong><br/><br/>

    <input type="submit" value="EXPORT!">
</form>

<br/>


<?php
if (!empty($_POST)) {

    /* ************************************************************** */
    file_put_contents($lastRepoDirFile, $repo); 

    $repoDir = $ini['base_repo_dir'] ."/$repo";
    $repoDir = rtrim($repoDir, '/\\');

    echo "<hr/>source repository: <strong>$repoDir</strong><br/>";
    echo "exporting to: <strong>$exportDir</strong><br/><br/>\n";


    createDir($exportDir);

    // empty export dir
    foreach (scandir($exportDir) as $file) {
        if ($file != '..' && $file != '.') {
            deleteDirRecursive("$exportDir/$file");
        }
    }

    // execute git diff
    $cmd = "git --git-dir=$repoDir/.git diff $range --name-only";

    exec("$cmd 2>&1", $output, $err);

    if ($err) {
        echo "Command error: <br/>";
        echo implode("<br/>", array_map('htmlspecialchars', $output));
        exit;
    }


    // $output contains a list of filenames with paths of changed files
    foreach ($output as $file) {

        $source = "$repoDir/$file";

        if (is_file($source)) {
            if (strpos($file, '/')) {
                createDir("$exportDir/" .dirname($file));
            }

            copy($source, "$exportDir/$file");
            echo "$file<br/>\n";

        } else {
            // deleted file
            echo "<span style='color: red'>$file</span><br/>\n";
        }
    }
}
?>

</body>
</html>

git-export.ini :

; path to all your git repositories for convenience - less typing
base_repo_dir = c:/www

; if you change it you have to also change it in the php script
; in deleteDirRecursive() function - this is for security
export_dir = c:/www/export

And now load localhost/git-export/ in a browser. The script is set up to export always to c:/www/export - change all paths to suit your environment or modify the script to suit your needs.

This will work if you have Git installed so that the git command is in your PATH - this can be configured when you run the windows Git installer.

Lemon Juice
  • 541
  • 1
  • 4
  • 3
  • this code is great, but i want export files and directories with submodules your code only worked with main repository and if submodules changed your code say submodule folder deleted! – morteza khadem Oct 12 '17 at 10:42
3

I also had the same problem. The solution of @NicolasDermine didn't work since too many files have changed between the compared commits. I got an error that the shell arguments are too long.

As a consequence I added a python implementation. This can be installed via pip install gitzip and then executed with

python -m gitzip export.zip <commit id> <commit id>

in the repositories root creating an export.zip file containing all changed files retaining the directory structure.

Maybe someone needs it too so I thought to share it here.

Disclaimer: I am the author of the gitzip module.

miile7
  • 2,547
  • 3
  • 23
  • 38
1

To export modified files starting with a date:

  diff --stat @{2016-11-01} --diff-filter=ACRMRT --name-only | xargs tar -cf 11.tar

Shortcut (use alias)

  git exportmdf 2016-11-01 11.tar

Alias in .gitconfig

  [alias]
  exportmdf = "!f() { \
    git diff --stat @{$1} --diff-filter=ACRMRT --name-only | xargs tar -cf $2; \
  }; f"
catalinp
  • 131
  • 2
  • 5
1

Here is a small bash(Unix) script that I wrote that will copy files for a given commit hash with the folder structure:

ARRAY=($(git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $1))
PWD=$(pwd)

if [ -d "$2" ]; then

    for i in "${ARRAY[@]}"
    do
        : 
        cp --parents "$PWD/$i" $2
    done
else
    echo "Chosen destination folder does not exist."
fi

Create a file named '~/Scripts/copy-commit.sh' then give it execution privileges:

chmod a+x ~/Scripts/copy-commit.sh

Then, from the root of the git repository:

~/Scripts/copy-commit.sh COMMIT_KEY ~/Existing/Destination/Folder/
Patrick.SE
  • 4,444
  • 5
  • 34
  • 44
0

I also faced a similar problem before. I wrote a simple Shell script.

$git log --reverse commit_HashX^..commit_HashY --pretty=format:'%h'

The above command will display Commit Hash(Revision) from commit_HashX to commit_HashY in reverse order.

 456d517 (second_hash)
 9362d03
 5362d03
 226x47a
 478bf6b (six_hash)

Now the main Shell Script using above command.

commitHashList=$(git log --reverse $1^..$2 --pretty=format:'%h')
for hash in $commitHashList
do
    echo "$hash"
    git archive -o \Path_Where_you_want_store\ChangesMade.zip $hash
done

Add this code to export_changes.sh. Now pass file and commit hashes to the script.

Make sure that initial commit_hash must be the first argument then last commit_hash up to which you want to export changes.

Example:

$sh export_changes.sh hashX hashY

Put this script into git local directory or set git local directory path in the script. Hope it Helps..!

0

This works for me for both Unix and Windows:

git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT __1__.. | xargs cp --parents -t __2__

There are two placeholders in the command. You need to replace them for your purpose:

  • __1__: replace with commit id of commit right before all of the commits you want to export (e.g. 997cc7b6 - don't forget to keep that doubledot after commit id - this means "involve all commits newer than this commit")

  • __2__: replace with existing path where you want to export your files (e.g. ../export_path/)

As a result you will get your files in a folder structure (no zips/tars...) as someone might be used to using tortoise svn exports in svn repositories.

This is for example pretty useful when you want to perform manual deploy of added/modified files of few last commits. So you can then just copy these files through ftp client.

Skybamar
  • 1
  • 1
0

git archive -o package.zip HEAD $(git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT <comit_id>)

Nathan Rona
  • 101
  • 9