Oscar the owl and Vershd logo
Menu

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

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 also have more general overview information about git stash.

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:

/**
* 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)
}
}
}

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!

Free

GitBreeze is an effortless Git GUI that's free for use at work, at home, anywhere. It boosts software development. It works on Windows, macOS, and Linux.

Designed for developers who want Git to be simple, our unique UI/UX boosts your software development - learn more and reap the benefits.

Help & tools

We provide these pages to try to make your programming life easier. Our resources page gives an overview. You can see a full list of our Git tips & tools here.

We have these free tools:

Customers say...

I love any tool that makes my life easier... This is perfect, just the right amount of control. No more, no less. Easy to get started, push and rollback changes... It's a no brainer!
Hayden T.
Oscar the owl and Vershd logo
 © 2024 GitBreeze Ltd.
St. George's flag of England, the Union Jack flag of the United Kingdom, the European Union flag, and the United Nations flag.
chevron-right linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram