Don’t let hidden Git stash files kill your code!

Man driving in invisible car.

Both ‘untracked’ and ‘ignored’ files are hidden when you look at the Git stash, and you could destroy important files when applying that stash item without even realising it. But you can see these files using NodeGit. And here’s how…

Git is a powerful and impressive version control system. Its just that sometimes, its less powerful in the ‘Dark Side of the Moon’ sense, and more powerful in the ‘four year old with a loaded hand gun’ sense. Git simply doesn’t give you all the information you might want easily, such as how do you see the ignored and untracked files in a Stash item? Well, here’s a method using NodeGit and TypeScript. We’re using this inside Vershd – our elegant Git GUI.

Showing Added, Modified and Deleted Files

Getting information out of Git Stash about individual files within it isn’t as easy as you might imagine. Whilst the command line (CLI) may provide ways to get this data, it isn’t complete. Both Git CMD and Git Bash fail to provide a complete picture of what’s going on. So does the otherwise estimable Tortoise Git. You may well be able to find all the files that were added, modified or deleted before you stashed them. Its just that you won’t be able to see any ignored or untracked files that were added to the stash.

By default, the CLI call that you may have made won’t add those sorts of files; you can add them using flags such as this to include the untracked files:

git stash push --include-untracked

And this to include the untracked and the ignored files:

git stash push --all

The only problem is ever seeing those files again, at least until you pop or apply the stash. In the meantime, you won’t be able to see them.

The solution involves using NodeGit to pull out this information from your local Git repository.

So first of all we create a class for file information, and do imports:

import * as NodeGit from 'nodegit'
 
// Create a class to hold the file information returned
public class MyFile {
    public newFilePath: string | undefined
    public oldFilePath: string | undefined
    public fileStatus: NodeGit.Diff.DELTA | undefined
    public lineAdditions: number
    public lineDeletions: number
}

And then we pull out the diffs between the stash item and its parent(s):

public class GetStashFiles {
    /**
     * Returns the list of diffs created by comparing the
     * stashSha with its parent commits.
     *
     * @param {string} repositoryLocation
     * @param {string} stashSha
     * @returns {Promise<NodeGit.Diff[]>}
     * @memberof GetStashFiles
     */
    public async getDiffs(repo: NodeGit.Repository,
                stashSha: string): Promise<NodeGit.Diff[]> {
        // Get the commit object
        const commit = await repo.getCommit(stashSha)
        // Get a diff for each of the parents of the commit
        return await commit.getDiff()
    }
}

These diffs are then in turn put into a method in the GetStashFiles class to get our ‘normal’ (added, modified and deleted) files out:

/**
 * Gets the added, modified, and deleted files in this stashSha stash item.
 * Needs the diffs returned by getDiffs() to work.
 *
 * @param {string} stashSha
 * @param {NodeGit.Diff[]} parentDiffs
 * @memberof GetStashFiles
 */
public async getNormalFiles(stashSha: string,
            parentDiffs: NodeGit.Diff[]): Promise<MyFile[]> {
    const myFiles = new Array<MyFile>()
 
    // Critically, we only need the diff against the first parent
    // to enable us to get the 'normal' files
    const patches = await parentDiffs[0].patches()
 
    // Now print out our results
    // NB: the old path is useful when a file has been renamed
    for (const patch of patches) {
        const myFile = new MyFile()
        myFile.fileStatus = patch.status()
        myFile.newFilePath = patch.newFile().path()
        myFile.oldFilePath = patch.oldFile().path()
        myFile.lineAdditions = patch.lineStats().total_additions
        myFile.lineDeletions = patch.lineStats().total_deletions
 
        console.log(`stashSha: ${stashSha
            }, newFilePath: ${myFile.newFilePath
            }, oldFilePath: ${myFile.oldFilePath
            }, status: ${myFile.fileStatus
            }, lineAdditions: ${myFile.lineAdditions
            }, lineDeletions: ${myFile.lineDeletions}`
        )
 
        myFiles.push(myFile)
    }
 
    return myFiles
}

And then create another method to run this code:

public async runCode(stashSha: string, parentDiffs: NodeGit.Diff[]) {
    const repo = await NodeGit.Repository.open(YourRepositoryLocation)
 
    const diffs = await this.getDiffs(repo, YourStashSha)
 
    const files = await this.getNormalFiles(YourStashSha, diffs)
}
 
// Resulting example console output
commit: e07968f20ffdbc88180ac35c2549dfb8f8aea744,
newFilePath: Added.txt, oldFilePath: Added.txt,
status: 1, lineAdditions: 1, lineDeletions: 0
 
commit: e07968f20ffdbc88180ac35c2549dfb8f8aea744,
newFilePath: Modified.txt, oldFilePath: Modified.txt,
status: 3, lineAdditions: 2, lineDeletions: 1
 
commit: e07968f20ffdbc88180ac35c2549dfb8f8aea744,
newFilePath: Deleted.txt, oldFilePath: Deleted.txt,
status: 2, lineAdditions: 0, lineDeletions: 1

Now of course, you can get something similar to this on your screen by typing a git stash show command into your Git CMD or Git Bash:

git stash show

Which would give you this result, with the the trailing number showing the overall amount of lines changed, and the plus and minus signs specifying exactly how many lines have been added or deleted:

Added.txt    | 1 +
Modified.txt | 3 ++-
Deleted.txt  | 1 -

But as mentioned, this only lists the ‘normal’ changes, and this is because you are viewing the changes of this stash item against its first parent. Even using the more in depth Git CLI command git stash show -p won’t help, although it will give details of how the files have changed:

git stash show -p

Giving the result:

diff --git a/Added.txt b/Added.txt
new file mode 100644
index 0000000..53bf775
--- /dev/null
+++ b/Added.txt
@@ -0,0 +1 @@
+added text
\ No newline at end of file
 
diff --git a/Modified.txt b/Modified.txt
index ca6854b..96b4039 100644
--- a/Modified.txt
+++ b/Modified.txt
@@ -1,2 +1,3 @@
 Some text line 1
-line 2
\ No newline at end of file
+line 2 text here
+line 3 text
\ No newline at end of file
 
diff --git a/Deleted.txt b/Deleted.txt
deleted file mode 100644
index 892eb83..0000000
--- a/Deleted.txt
+++ /dev/null
@@ -1 +0,0 @@
-Text on line 1
\ No newline at end of file

Showing the Ignored and Untracked Files

So how do you get to see these files? By returning to the stash item’s parent commits and seeing what lurks in there. This method is added onto the class:

<pre class="wp-block-syntaxhighlighter-code">/**
 * Show the ignored and untracked files in this stashSha stash item.
 *
 * @param {NodeGit.Repository} repository
 * @param {MyFile[]} normalFiles
 * @param {string} stashSha
 * @param {NodeGit.Diff[]} parentDiffs
 * @memberof GetStashFiles
 */
public async getIgnoredAndUntrackedFiles(
    repository: NodeGit.Repository,
    normalFiles: MyFile[],
    stashSha: string,
    parentDiffs: NodeGit.Diff[]
) {
    const myFiles = new Array<MyFile>()
 
    // Get the ignored and untracked files from the second parent onwards
    for (let i = 1; i < parentDiffs.length; i++) { const newPatches = await parentDiffs[i].patches() for (const patch of newPatches) { // This is where it gets weird; the statuses of the files are // in effect 'reversed' against their parent commit, but there // is no way to invoke NodeGit.Diff.OPTION.REVERSE on // commit.getDiff(). The result is that all of the unmodified // files for example are labelled as 'added'. In fact, we only // want the 'deleted' files not already discovered, which are // actually either ignored or untracked. if ( patch.status() !== NodeGit.Diff.DELTA.DELETED || normalFiles.some( f => f.newFilePath === patch.newFile().path())
            ) {
                continue
            }
 
            const isIgnored = await NodeGit.Ignore.pathIsIgnored(
                repository,
                patch.newFile().path()
            )
 
            const realStatus = isIgnored
                ? NodeGit.Diff.DELTA.IGNORED
                : NodeGit.Diff.DELTA.UNTRACKED
 
            // Switch the lines added / deleted (remember the statuses
            // and lines changed have effectively been reversed)
            const linesDeleted = patch.lineStats().total_additions
            const linesAdded = patch.lineStats().total_deletions
 
            const myFile = new MyFile()
            myFile.fileStatus = realStatus
            myFile.newFilePath = patch.newFile().path()
            myFile.oldFilePath = patch.oldFile().path()
            myFile.lineAdditions = linesAdded
            myFile.lineDeletions = linesDeleted 
 
            console.log(`stashSha: ${stashSha
                }, newFilePath: ${myFile.newFilePath
                }, oldFilePath: ${myFile.oldFilePath
                }, status: ${myFile.fileStatus
                }, lineAdditions: ${myFile.lineAdditions
                }, lineDeletions: ${myFile.lineDeletions}`
            )
 
            myFiles.push(myFile)
        }
    }
}</pre>

We then add to the runCode method to get this new information logged:

public async runCode(stashSha: string, parentDiffs: NodeGit.Diff[]) {
    const diffs = await this.getDiffs(YourRepositoryLocation, YourStashSha)
    const files = await this.getNormalFiles(YourStashSha, diffs)
 
    // New code - get the repository object and then the new files
    const repo = await NodeGit.Repository.open(repositoryLocation)
    this.getIgnoredAndUntrackedFiles(repo, files, YourStashSha, diffs)
}
// Resulting example console output
commit: stashSha: e07968f20ffdbc88180ac35c2549dfb8f8aea744,
newFilePath: IgnoredFile.txt, oldFilePath: IgnoredFile.txt,
status: 6, lineAdditions: 1, lineDeletions: 0
 
stashSha: e07968f20ffdbc88180ac35c2549dfb8f8aea744,
newFilePath: Untracked.txt, oldFilePath: Untracked.txt,
status: 7, lineAdditions: 1, lineDeletions: 0

Conclusion

So we’ve seen how to add in the ignored and untracked files in the Git stash. They are tucked away, in parent commits of the original stash item. And they are labelled incorrectly. But with care and attention, we can pull them out using NodeGit and get useful information from them.

Happy coding!

We're busy creating Vershd, which is the elegant Git GUI.

Tagged with: , , , , , , , ,