Rory Primrose

Learn from my mistakes, you don't have time to make them yourself

View project on GitHub

Namespace Renamer

Posted on April 14, 2015

All the way back in 2009 I wrote a little console application that assisted with renaming a Visual Studio project and/or namespace. It was handy in that it took into consideration the solution being under source control with TFS. Over the years I have been using TF source control less and Git more. On the odd occasion I have also used this script for solutions that are not bound in source control.

I have refactored the script to take out the TF integration, remove the logging that was just causing a whole lot of white noise and to add in a few more out of the box exclusions for directories to skip in the process.

namespace Neovolve.NamespaceRenamer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Text;
    using System.Text.RegularExpressions;

    internal class Program
    {
        /// <summary>
        ///     The name to find.
        /// </summary>
        private const string FindName = "Solution.Project.OldNamespace";

        /// <summary>
        ///     The name to replace.
        /// </summary>
        private const string ReplaceName = "Solution.Project.NewNamespace";

        /// <summary>
        ///     The root path from which changes are made.
        /// </summary>
        private const string RootPath = @"D:\Repos\Solution.Project";

        /// <summary>
        ///     Stores the directories that should not be processed
        /// </summary>
        private static readonly List<string> DirectoryExclusions = new List<string>
        {
            "bin", 
            "obj", 
            "$tf", 
            ".git", 
            "Packages", 
            "References", 
            "TestResults"
        };

        /// <summary>
        ///     Stores the file extensions that should not be processed.
        /// </summary>
        private static readonly List<string> FileExtensionExclusions = new List<string>
        {
            ".dll", 
            ".exe", 
            ".pdb",
            ".suo"
        };

        private static readonly string LogPath = Assembly.GetExecutingAssembly().Location + "."
                                                 + DateTime.Now.ToString("yyyyMMdd-hhmmss") + ".log";

        private static Regex _expression;

        private static Regex DetermineFindReplaceExpression(string findText, string replaceText)
        {
            string pattern;

            if (replaceText.EndsWith(findText, StringComparison.Ordinal))
            {
                // The rename is to add a prefix
                string prefix = replaceText.Substring(0, replaceText.Length - findText.Length);
                pattern = "(?<!" + Regex.Escape(prefix) + ")" + Regex.Escape(FindName);

                WriteMessage("Rename with new prefix detected");
            }
            else if (replaceText.StartsWith(findText, StringComparison.Ordinal))
            {
                // The rename is to add a suffix
                string suffix = replaceText.Substring(findText.Length);
                pattern = Regex.Escape(FindName) + "(?!" + Regex.Escape(suffix) + ")";

                WriteMessage("Rename with new suffix detected");
            }
            else if (replaceText.Contains(findText))
            {
                // The rename is to add a prefix and a suffix
                string prefix = replaceText.Substring(0, replaceText.IndexOf(findText));
                string suffix = replaceText.Substring(replaceText.LastIndexOf(findText) + findText.Length);
                pattern = "(?<!" + Regex.Escape(prefix) + ")" + Regex.Escape(FindName) + "(?!" + Regex.Escape(suffix) +
                          ")";

                WriteMessage("Rename with new prefix and suffix detected");
            }
            else
            {
                // The replace text is not contained within the find text
                pattern = Regex.Escape(FindName);

                WriteMessage("Rename with no suffix or prefix detected");
            }

            return new Regex(pattern, RegexOptions.Compiled);
        }

        private static void Main(string[] args)
        {
            _expression = DetermineFindReplaceExpression(FindName, ReplaceName);

            WriteMessage("Processed started at " + DateTime.Now);

            RunRename(new DirectoryInfo(RootPath));

            WriteMessage("Processed completed at " + DateTime.Now);
            WriteMessage("Log file located at " + LogPath);

            Console.ReadKey();
        }

        private static void RenameDirectory(DirectoryInfo directory)
        {
            WriteMessage("Renaming directory " + directory.FullName);

            Debug.Assert(directory.Parent != null, "No parent directory available for the directory");

            string newName = _expression.Replace(directory.Name, ReplaceName);
            string newPath = Path.Combine(directory.Parent.FullName, newName);

            directory.MoveTo(newPath);
        }

        private static void RenameFile(FileInfo file)
        {
            WriteMessage("Renaming file " + file.FullName);

            Debug.Assert(file.Directory != null, "No parent directory available for the file");

            string newName = _expression.Replace(file.Name, ReplaceName);
            string newPath = Path.Combine(file.Directory.FullName, newName);

            file.MoveTo(newPath);
        }

        private static void RunRename(DirectoryInfo directory)
        {
            if (DirectoryExclusions.Contains(directory.Name))
            {
                WriteMessage("Skipping excluded directory " + directory.FullName);

                return;
            }

            // Check if the directory is not excluded
            foreach (DirectoryInfo childDirectory in directory.GetDirectories())
            {
                // Recurse into child directories
                // Recurse first before changing the current directory results in a bottom up change
                RunRename(childDirectory);
            }

            foreach (FileInfo file in directory.GetFiles())
            {
                if (FileExtensionExclusions.Contains(file.Extension))
                {
                    WriteMessage("Skipping " + file.FullName + " due to file extension exclusion");

                    continue;
                }

                // Load the contents
                string contents = File.ReadAllText(file.FullName, Encoding.UTF8);

                if (_expression.IsMatch(contents))
                {
                    contents = _expression.Replace(contents, ReplaceName);

                    WriteMessage("Updating file contents for " + file.FullName);

                    // Ensure that the file is not read-only
                    // This will occur when the file was not under source control
                    if (file.IsReadOnly)
                    {
                        file.IsReadOnly = false;
                    }

                    File.WriteAllText(file.FullName, contents, Encoding.UTF8);
                }

                // Check if the file needs to be renamed
                if (_expression.IsMatch(file.Name))
                {
                    RenameFile(file);
                }
            }

            if (_expression.IsMatch(directory.Name))
            {
                RenameDirectory(directory);
            }
        }

        private static void WriteMessage(string message)
        {
            Console.WriteLine(message);
            File.AppendAllText(LogPath, message + Environment.NewLine);
        }
    }
}

All you have to do is drop this into a new console project, change the three field values and off you go.