Rory Primrose

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

View project on GitHub

Namespace Renamer

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.

Written on April 14, 2015