private static FilenameAndHashValues GenerateFileHashes(PromotionGroupDetails promotionGroupDetails, string filenameListEntry)
        {
            string promotionGroupFilePath = Path.Combine(promotionGroupDetails.PromotionGroupSharePath, filenameListEntry);
            string repositoryFilePath     = Path.Combine(promotionGroupDetails.GitRepoSharePath, filenameListEntry);

            return(new FilenameAndHashValues(filenameListEntry,
                                             new NonEolHash(promotionGroupFilePath).Generate(),
                                             new NonEolHash(repositoryFilePath).Generate()));
        }
        static int Main(string[] args)
        {
            int error = 1;

            if (args.Length < 2)
            {
                Console.WriteLine();
                Console.WriteLine("Usage: AnalyseDifferences PromotionGroup InputFilename {InputFilename...}");
            }
            else
            {
                // At least two program arguments

                string selectedPromotionGroup = args[0];

                if (!_PromotionGroupDetailsSortedDictionary.Keys.Contains(selectedPromotionGroup))
                {
                    Console.WriteLine();
                    Console.WriteLine("Promotion Group \"{0}\" does not exist", selectedPromotionGroup);
                }
                else
                {
                    // Selected Promotion Group exists

                    Console.WriteLine();
                    Console.WriteLine("Selected Promotion Group is {0}", selectedPromotionGroup);
                    Console.WriteLine();

                    PromotionGroupDetails selectedPromotionGroupDetails = _PromotionGroupDetailsSortedDictionary[selectedPromotionGroup];

                    // Assume overall success unless otherwise determined
                    error = 0;

                    // Start after the Promotion Group specification
                    for (int argId = 1; argId < args.Length; ++argId)
                    {
                        string inputFilename = args[argId];

                        inputFilename = Path.GetFullPath(inputFilename);

                        if (!File.Exists(inputFilename))
                        {
                            Console.WriteLine();
                            Console.WriteLine("File \"{0}\" does not exist", inputFilename);
                        }
                        else
                        {
                            // Input file exists

                            // Assume success for this Promotion Group and change the error if it isn't
                            int fileDifferenceCount = 0;

                            using (StreamReader fileInput = new StreamReader(inputFilename))
                            {
                                string line           = null;
                                int    lineNumber     = 0;
                                bool   linesProcessed = false;
                                bool   finished       = false;
                                while ((!finished) && (line = fileInput.ReadLine()) != null)
                                {
                                    string listEntry = line;
                                    lineNumber += 1;

                                    // Trim all white space (tabs and spaces)
                                    listEntry = listEntry.Trim();

                                    // Skip lines beginning with a hash character
                                    if (listEntry.Length == 0)
                                    {
                                        if (linesProcessed)
                                        {
                                            // Encountering an empty line after having processed non-blank filenames means the task is complete
                                            finished = true;
                                        }
                                    }
                                    else if (listEntry[0] != '#')
                                    {
                                        // Valid list entry and List Entry is not commented out

                                        // Tidy up the entry and convert to a Windows suitable format
                                        listEntry = listEntry.Replace("/", "\\");
                                        // No leading or trailing backslashes
                                        listEntry = listEntry.Trim(new char[] { '\\' });

                                        // Input line is of the format "{whitespace}gitstatus:{whitespace}filename

                                        // Locate the filename which must follow a colon
                                        int posColon = listEntry.IndexOf(':');
                                        if (posColon < 0)
                                        {
                                            if (linesProcessed)
                                            {
                                                // Lines have been previously processed so unexpectedly a filename has not been specified
                                                Console.WriteLine();
                                                Console.WriteLine("\"{0}\"({1}) : Expected a file specification but none was present",
                                                                  inputFilename, lineNumber);
                                                Console.WriteLine();
                                            }
                                        }
                                        else
                                        {
                                            // A file specification appears to be present

                                            // Guard against trailing colons at the end of the line
                                            if ((posColon + 1) < listEntry.Length)
                                            {
                                                // A filename really does appear to be present
                                                linesProcessed = true;

                                                // Parse out the "git status" action
                                                string[] listEntryItem = listEntry.Split(new char[] { ' ', ':' }, StringSplitOptions.RemoveEmptyEntries);

                                                string gitStatusAction = listEntryItem[0];

                                                // Parse out the filename allowing for filenames with spaces and remove leading and trailing whitespace
                                                string gitRepoFilename = listEntry.Substring(posColon + 1).Trim();

                                                // Only compare if there is a file to compare
                                                if (String.Compare(gitStatusAction, "deleted", true /* ignore case */) == 0)
                                                {
                                                    Console.WriteLine("Promotion Group file \"{0}\" has been deleted relative to the Git Repository", gitRepoFilename);
                                                    // The Promotion Group Directory and the Repository Working Directory should contain the same files
                                                    fileDifferenceCount += 1;
                                                }
                                                else
                                                {
                                                    // Compare the files in each Promotion Group Volume and the corresponding Git Repository Working Directory

                                                    string sourceName = Path.Combine(selectedPromotionGroupDetails.PromotionGroupSharePath, gitRepoFilename);
                                                    if (File.Exists(sourceName))
                                                    {
                                                        // Use a hash to attempt to decide whether the file contents are actually different
                                                        FilenameAndHashValues filenameAndHashValues = GenerateFileHashes(selectedPromotionGroupDetails, gitRepoFilename);
                                                        if (filenameAndHashValues.PromotionGroupFileHash != filenameAndHashValues.RepositoryFileHash)
                                                        {
                                                            // The file hashes are different
                                                            // However, if the files are Solutions perhaps they are actually semantically equivalent
                                                            bool     thisFileIsTheSame = false;
                                                            FileInfo fileInfo          = new FileInfo(sourceName);
                                                            if (String.Compare(fileInfo.Extension, ".sln", true /*ignore case*/) == 0)
                                                            {
                                                                bool exactlyEquivalent      = false;
                                                                bool semanticallyEquivalent = false;
                                                                AnalyseDifferences.CompareSolutions.Compare(gitRepoFilename,
                                                                                                            selectedPromotionGroupDetails.GitRepoSharePath,
                                                                                                            selectedPromotionGroupDetails.PromotionGroupSharePath,
                                                                                                            ref exactlyEquivalent,
                                                                                                            ref semanticallyEquivalent);
                                                                // It is sufficient that the Solutions "mean" the same thing rather than "be" exactly the same
                                                                thisFileIsTheSame = semanticallyEquivalent;
                                                                Console.WriteLine("Solution comparison for \"{0}\" indicates semantic {1}",
                                                                                  gitRepoFilename,
                                                                                  (thisFileIsTheSame ? "equivalence" : "difference"));
                                                            }
                                                            if (!thisFileIsTheSame)
                                                            {
                                                                fileDifferenceCount += 1;
                                                                Console.WriteLine("Files for \"{0}\" appear to be different", filenameAndHashValues.Filename);
                                                            }
                                                        } // The file hashes are different
                                                        else
                                                        {
                                                            //Console.WriteLine("Files for \"{0}\" appear to be the same. Hash = {1}",
                                                            //                    filenameAndHashValues.Filename, filenameAndHashValues.PromotionGroupFileHash);
                                                        }
                                                    }
                                                    else
                                                    {
                                                        Console.WriteLine("Promotion Group file \"{0}\" has been deleted relative to the Git Repository", gitRepoFilename);
                                                        // The Promotion Group Directory and the Repository Working Directory should contain the same files
                                                        fileDifferenceCount += 1;
                                                    }
                                                } // Compare the files in each Promotion Group Volume and the corresponding Git Repository Working Directory
                                            }     // A filename really does appear to be present
                                        }         // A file specification appears to be present
                                    }             // Valid list entry and List Entry is not commented out
                                }                 // while not end-of-file
                            }                     // using StreamReader

                            if (fileDifferenceCount == 0)
                            {
                                Console.WriteLine();
                                Console.WriteLine("No differences were detected between the Promotion Group Volume \"{0}\" and the corresponding Git Repository \"{1}\"",
                                                  selectedPromotionGroupDetails.PromotionGroupSharePath, selectedPromotionGroupDetails.GitRepoSharePath);
                            }
                            else
                            {
                                Console.WriteLine();
                                Console.WriteLine("Number of differences detected between the Promotion Group Volume \"{0}\" and the corresponding Git Repository \"{1}\" was {2}",
                                                  selectedPromotionGroupDetails.PromotionGroupSharePath, selectedPromotionGroupDetails.GitRepoSharePath, fileDifferenceCount);
                                error = 1;
                            }
                        } // Selected Promotion Group exists
                    }     // for argId
                }         // Input file exists
            }             // At least two program arguments

            return(error);
        }