Tuesday, March 23, 2021

Remove git orphan branches for *nix based OS.

I've already posted how to remove orphan branches if you develop on windows. This post is just a small addition but for those who develops on *nix based OSes. 

The problem is very simple, you develop something, you push it, you get your branch merged, then you need manually remove local branches. It can be easily automated, first we need to run

git branch -vv

and then remove all branches that have no remote branches.

git branch -D branch_name

It's possible to write a simple bash script that checks git branches and then just removes ones that no longer have remote branches. I'm not a huge fan os bash so I decided to go with python. It's more convenient, predictable and easier to use. Bellow is the script itself.

#!/usr/bin/env python
import subprocess

cmd_output = subprocess.check_output('git branch -vv', shell=True)

output_lines = cmd_output.split('\n')

to_remove = []

for line in output_lines:
    if line.startswith('*'): # skip current branch
        continue

    if 'gone' in line: # there is no remote branch
        line_segments = line.strip().split(' ')
        branch = line_segments[0]
        to_remove.append(branch)

if not to_remove:
    print('There is nothing to remove, consider running `git fetch` and `git remote prune origin`')

for branch in to_remove:
    subprocess.call('git branch -D ' + branch, shell=True)

Let's save this file as git-clean. Of cause you can use whatever name your want, but then please adjust the instructions bellow. 

In order to make it more convenient to use let's make it executable by running 

chmod +x git-clean

and add this executable to the PATH variable. Simply go to your home folder and find there the .bash_profile. Open it and add the path to the folder where your git-clean is located. In my case it's ~/projects/tools.

export PATH="~/projects/tools:$PATH"

Now restart your terminal and  git-clean should be globally available.

P.S. this script uses python 2.x. and with python 3.x is should be possible to achieve the same result easier.


Tuesday, September 3, 2019

Remove git orphan branches

Quite often especially when you create a pull request for some short time you have a local branch and a corresponding remote one that you track. But after your pull request is merged and remote branch is removed you still have your local branch.

If you run command

git remote prune origin

It will check if branches that you track are still there, if not  then your local branches are orphans now.

If you run now

git branch -vv

Then you will get a nice info about each branch


Here we can see that branch_a is gone, git literally says it. This is our orphan branch. No we can run the following command to remove it.

git branch -D branch_a

But what if we have 5 or 10 branches that became orphans? It would be nice to have some helper function that automates this process.

If your working environment is windows you can easily add a helper function into your powershell profile. Just go to powershell console and run

notepad $profile

And add the code bellow at the end of your powershell profile.

function git-clean() {
 git branch -vv | Where {$_.Contains("gone")} | ForEach -Process { git branch -D $_.Split()[2] }
}

Now every time you open a powershell console it will scan your profile and import all functions declared there. Now you can simply run git-clean and it will do all magic.

As summary just run


git remote prune origin
git-clean

P.S. if you have just added this function you need first to restart you powershell console to make this function available.

Friday, November 3, 2017

Recharts bar chart with multiple colors

I needed once to add a bar chart using Recharts  library but with multiple colors, with two coloros rotated in the bar. Something like this:



There is no such functionality out of the box, but it's doable. Recharts provide a possibility to customize bar shape. Bar component has an attribute shape that accepts a function or a component, to render the shape of the bar.

Since Recharts uses SVG under the hood the first thing I did I found how people get such shapes using pure SVG. To achive that we need to define 2 things: a pattern and a mask based on that pattern.

<svg width="120" height="120" viewBox="0 0 120 120"
    xmlns="http://www.w3.org/2000/svg">

  <!-- Pattern -->
  <pattern id="pattern-stripe" 
        width="8" height="8" 
         patternUnits="userSpaceOnUse"
         patternTransform="rotate(45)">
    <rect width="4" height="8" transform="translate(0,0)" fill="white"></rect>
  </pattern>
  <!-- Mask -->
  <mask id="mask-stripe">
   <rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-stripe)" />
  </mask>
  
  <!-- Reactangle that uses mask -->
  <rect x="10" y="10" width="100" height="100" mask='url(#mask-stripe)' />
</svg>

I put the mask and the pattern in the SVG tag and also a rect tag that uses the mask to define how this react will be filled. If you save it as SVG file and open in a browser you will see something like this:


It's pure SVG and it has nothing to do with react and recharts. However we already know that we are on the right track.
Let's create a function that will render a colored rectangle:

const CandyBar = (props) => {
 const {    
    x: oX,
    y: oY,
    width: oWidth,
    height: oHeight,
    value,
    fill
  } = props;
  
  let x = oX;
  let y = oHeight < 0 ? oY + oHeight : oY;
 let width = oWidth;
  let height = Math.abs(oHeight);

 return (
   <rect fill={fill}
          mask='url(#mask-stripe)'
          x={x}
          y={y}
          width={width}
          height={height} />
    );
};

No we can use it as shape parameter for the Bar component. There is nothing interesting here, we just so cover here the case when height is negative, it happens when we need to display a negative value.  We also set here the mask, to define how the react will be filled. This is how looks a bar with custom shape:

 <Bar dataKey="a" fill="green" shape={<CandyBar />} />

Now let's put everything together:

const data = [
      {name: 'Page A', a: 400, b: 240, c: 240},
      {name: 'Page B', a: 300, b: 139, c: 221},
      {name: 'Page C', a: -200, b: -980, c: 229},
      {name: 'Page D', a: 278, b: 390, c: 200},
      {name: 'Page E', a: 189, b: 480, c: 218},
      {name: 'Page F', a: 239, b: -380, c: 250},
      {name: 'Page G', a: 349, b: 430, c: 210}
];

const CandyBar = (props) => {
  const {    
    x: oX,
    y: oY,
    width: oWidth,
    height: oHeight,
    value,
    fill
  } = props;
  
  let x = oX;
  let y = oHeight < 0 ? oY + oHeight : oY;
  let width = oWidth;
  let height = Math.abs(oHeight);

  return (
   <rect fill={fill}
       mask='url(#mask-stripe)'
          x={x}
          y={y}
          width={width}
          height={height} />
    );
};



const CustomShapeBarChart = React.createClass({
  render () {
    return (
    <div>    
      <BarChart width={700} height={300} data={data}
            margin={{top: 20, right: 30, left: 20, bottom: 5}}>
        
        <pattern id="pattern-stripe" 
         width="8" height="8" 
         patternUnits="userSpaceOnUse"
         patternTransform="rotate(45)">
         <rect width="4" height="8" transform="translate(0,0)" fill="white"></rect>
        </pattern>
        <mask id="mask-stripe">
        <rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-stripe)" />
        </mask>

         <XAxis dataKey="name"/>
         <YAxis/>
         <CartesianGrid strokeDasharray="3 3"/>
         <Bar dataKey="a" fill="green" isAnimationActive={false} shape={<CandyBar />} />
         <Bar dataKey="b" isAnimationActive={false} fill="red" />
         <Bar dataKey="c" isAnimationActive={false} shape={<CandyBar fill="#8884d8" />} />
      </BarChart>
    </div>
    );
  }
})

ReactDOM.render(
  <CustomShapeBarChart />,
  document.getElementById('container')
);

It will render fallowing chart:



Here is a working fiddle

There are several important things here:
1. BarChart component generates SVG tag.
2. Mask and Pattern tags should be inside SVG tag. That is why we put them as children of the BarChart
3. Bar component uses CandyBar to define how the shape will be rendered.
4. CandyBar generates a rect with the proper mask that is located by Id.

Sunday, August 6, 2017

React datetime component, disable keyboard input

We currently use react on our current project and react-datetime library to render datetime picker.



Recently we got a requirment to disable keyboard input, so that users can choose a date only from the calendar UI. Unfortunately this functionality is not availbale out of the box. But there is a way to do it.

ReactDOM package provides a method to locate html element that contains react component.

ReactDOM.findDOMNode(this);

The simplest solution would be to wrap <Datetime /> component with a custom one and then disable user input, simply suppress all keyboard interaction.

Each react component has a set of hooks , one of them is componentDidMount. This method is invoked once when component is mounted. This is the right place to do some magic.

componentDidMount() {
  const componentPlaceholder = ReactDOM.findDOMNode(this);
  $(componentPlaceholder)
   .find('input')
   .on('keydown', () => false);
}

We simply search for the html element that contains the component and then search for an input inside of that element. Then we add event listenet for keydown event and suppres all keys. It would probably make sense to add some exceptions for some special keys like F11, F12, etc.

Full component code:


import React from 'react';
import ReactDOM from 'react-dom';
import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css';

export default class DateTimePicker extends React.Component {

    componentDidMount() {
      const componentPlaceholder = ReactDOM.findDOMNode(this);
      $(componentPlaceholder)
          .find('input')
          .on('keydown', () => false);
    }

    render() {
        return <Datetime {...this.props} />;
    }
}

Thursday, December 29, 2016

Unity and Quartz .NET

In my current project we need to use Quartz .NET for scheduled tasks. Since we already use Unity IoC it makes sense to inject all dependencies into quartz jobs. After googling I found this repository but example in that soulution as well as the nuget package keep throwing exceptions. Turned out to be that in order to integrate any IoC container with quartz it's enough to do three simple steps.

1. Implement IJobFactory interface

public class UnityJobFactory : IJobFactory
{
    private readonly IUnityContainer _container;

    public UnityJobFactory(IUnityContainer container)
    {
        _container = container;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        return _container.Resolve(bundle.JobDetail.JobType) as IJob;
    }

    public void ReturnJob(IJob job)
    {
    }
}

2. Inherit StdSchedulerFactory

public class UnitySchedulerFactory : StdSchedulerFactory
{
    private readonly IJobFactory _jobFactory;

    public UnitySchedulerFactory(IJobFactory jobFactory)
    {
        _jobFactory = jobFactory;
    }

    protected override IScheduler Instantiate(QuartzSchedulerResources rsrcs, QuartzScheduler qs)
    {
        qs.JobFactory = _jobFactory;

        return base.Instantiate(rsrcs, qs);
    }
}

3. Register those two implementations in the container

container.RegisterType<IJobFactory, UnityJobFactory>();

container.RegisterType<ISchedulerFactory, UnitySchedulerFactory>();

In adition to keep everything more separated, independat and easier to use, we can extract registration into a Unity extension.

public class QuartzUnityExtension : UnityContainerExtension
{
    protected override void Initialize()
    {
        this.Container.RegisterType<IJobFactory, UnityJobFactory>();

        this.Container.RegisterType<ISchedulerFactory, UnitySchedulerFactory>();
    }
}

And of cause an example.


public class MyJob : IJob
{
    private readonly ISomeService _someService;

    public MyJob(ISomeService someService)
    {
        _someService = someService;
    }

    public void Execute(IJobExecutionContext context)
    {
        Console.WriteLine(_someService.GiveMeSomething());
    }
}

public interface ISomeService
{
    string GiveMeSomething();
}

public class SomeService : ISomeService
{
    public string GiveMeSomething()
    {
        return "Something";
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var container = new UnityContainer();

        container.AddNewExtension<QuartzUnityExtension>();

        container.RegisterType<ISomeService, SomeService>();

        var scheduler = container.Resolve<ISchedulerFactory>().GetScheduler();

        scheduler.ScheduleJob(
            new JobDetailImpl("myJob", typeof(MyJob)),
            new CalendarIntervalTriggerImpl("TestTrigger", IntervalUnit.Second, 2)
        );

        scheduler.Start();

        Thread.Sleep(TimeSpan.FromSeconds(10));

        scheduler.Shutdown();
    }
}

Friday, October 23, 2015

Nodejs 4.x.x on Raspberry Pi

Simple steps to install nodejs 4.x.x on Raspberry Pi 2:

sudo apt-get update

sudo apt-get upgrade

curl -sL https://deb.nodesource.com/setup | sudo bash -

sudo apt-get install nodejs

I got version 0.10.4 yours can be different.

Let's update it to the version 4.x.x:

sudo npm install -g n

sudo n stable

Restart the terminal and type

node -v

I got version v4.2.1

P.S. OS version on my Raspberry Pi:

Distributor ID: Debian
Description:    Debian GNU/Linux 7.8 (wheezy)
Release:        7.8
Codename:       wheezy

You can check your version simply by typing:

lsb_release -a

Tuesday, October 13, 2015

Nodejs 4.x.x on Debian 7 wheezy

If you use debian 7 wheezy and cannot install nodejs of version 4.x.x most likely it's because you have a wrong version of gcc. Nodejs 4.x.x requires gcc 4.8 or higher. Unfortunately Debian has gcc 4.8 starting from version 8 jessie. Fortunately there is a way to install it.

In order to install you need to adjust sources.list file in /etc/apt. Just add on line at the bottom of this file:

deb http://ftp.uk.debian.org/debian/ jessie main non-free contrib

Now check if you have preferences file in /etc/apt. If no then just create one.
Add following content to this file:

Package: *
Pin: release n=wheezy
Pin-Priority: 900

Package: gcc*
Pin: release n=jessie
Pin-Priority: 910

Now just use aptitude to install required version of gcc

sudo aptitude update
sudo aptitude install gcc/jessie

Thursday, October 1, 2015

Interesting behavior in Moment.js

Momemnt.js is a great library to work with dates. One of the features that I really like is ability to parse dates in different formats. Recently our QA has found that dates in short format, where the year is represented by 2 digits behave differently. For example date format is DD.MM.YY in this case we will have following results:

'12.05.88' becomes Thu May 12 1988 00:00:00 GMT+0200 (W. Europe Daylight Time)
'12.05.52' becomes Sun May 12 2052 00:00:00 GMT+0200 (W. Europe Daylight Time)

We got two dates in different centuries.
Turned out to be there is a special function called parseTwoDigitYear and default implementation is:


hooks.parseTwoDigitYear = function (input) {
    return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
};

It means that all years after 68 will be in 20th century but 68 and everything bellow 68 will go to the 21st. What is also interesting that 68 is just a random number. Good thing is that it could be adjusted.
You can override this behavior. As example let's make it return always the year 1995.


moment.parseTwoDigitYear = function (input) {
    return 1995;
};

You can find more about customization here

Tuesday, July 21, 2015

JSCS for different platforms

We use jscs in our project. It's nice tool, and has nice feature, it allows to use configuration files from the other teams. We decided to take one from Airbnb. Everything was great, but there is one problem, some developers in our team use Windows environments, other Mac OS, and our build machine is hosted on Debian. It brings us the situation that some machine have CRLF line endings and some LF endings. And we need some way to use jscs on the all environments. Since we use gulp task manager, it was logical to include jscs check in our build process, and in order to make it work we just put one if statement that overrides line endings rule based on the OS where the script is executed.

.jscsrc file

{
  "preset": "airbnb",
  "fileExtensions": [ ".js" ],
  "validateLineBreaks": 'LF'
}

Function inside gulp files, that reads and adjust if need .jscsrc


function readRules() {
  return new Promise((resolve, reject) => fs.readFile('.jscsrc', (error, data) => {
    if (error) {
      reject(error);
    }
    else {
      let config = JSON.parse(data);
      if (os.platform() === 'win32') {
        config.validateLineBreaks = 'CRLF'
      }

      resolve(config);
    }
  }));
}

It was fine and worked fine for us. Another problem that we ran into was Webstorm support. Webstorm is a very cool IDE for javascript and turned out to be, that it also supports jscs validation out of the box. You just need go to the settings and enable it. But the same issue as before, we have different environments and Webstorm parses all sources and applies jscs config to each of them, but there is now way to apply any conditions. We didn't find any nice way to tune that behavior for Webstorm, We decided to simply ignore line endings during development process in IDE but still have it as a part of our build script. So we just disable that rule completely by setting  validateLineBreaks to null.

.jscsrc file that suppress validateLineBreaks from parent configuration(airbnb)

{
  "preset": "airbnb",
  "fileExtensions": [ ".js" ],
  "validateLineBreaks": null
}

It means that validation by Webstorm, which simply runs node.js under the hood and passes jscssrc.json as a parameter, will consider all line endings to be correct. OK, great, one small thing to change is gulp task, to put both line endings into play, instead of overriding as before, since now jscs has it to be set to NULL.

Adjusted function in gulp file that set correct line endings for both OS's


function readRules() {
  return new Promise((resolve, reject) => fs.readFile('.jscsrc', (error, data) => {
    if (error) {
      reject(error);
    }
    else {
      let config = JSON.parse(data);

      config.validateLineBreaks = os.platform() === 'win32' ? 'CRLF' : 'LF';

      resolve(config);
    }
  }));
}

Some things to mention:
1. Since we take Airbnb version as base one, we get settings where validateLineBreaks set to LF
2. Why do we care about line endings at all...well it's a good practice and we want to be consistent.
3. How does it work between environments?! Git does the magic. We simply set autocrlf setting to true. It means that every time somebody checkout sources under Windows OS git normalized all files and replace LF with CRLF, on *NIX environments git replace CRLF line endings with LF, of cause if there are such.

Summary:
1. There is no way to put some conditions into jscsrc
2. By setting jscs rule to null we can disable it completely
2. Some OS specific validation like validateLineBreaks could be a part of a build task(gulp, grunt, etc.)

Friday, December 12, 2014

NancyFX global error handling

Add global error handling to NancyFX Web API is very easy. We need to add a Bottstapper class and inherit it from DefaultNancyBootstrapper. Let's override RequestStartup method.


public class Bootstrapper : DefaultNancyBootstrapper
{
    private ILogger logger;

    protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
    {
        pipelines.OnError += (ctx, e) =>
        {
            this.logger.Error(e.Message, e);

            return null;
        };

        base.RequestStartup(container, pipelines, context);
    }
}

This method is executed on each call, if any unhandled exception happens we will end up here.

Monday, November 24, 2014

Nancy shared module

I was recently involved in development of a new system that heavily uses NancyFX as Rest API framework. System has a couple of different rest APIs. At some point we decided to show somehow a version of each service and since we have around 10 different services, it'd be nice to have some shared functionality. Our solution is quite simple, we created a new solution with class library inside, added NancyFX as a dependency using Nuget and added one new class VersionModule:


public class VersionModule : NancyModule
{
    public VersionModule() : base("/version")
    {
        Get["/"] = _ => Response.AsJson(new {Text = "Here shoul be some usefull info"});
    }
}

If we create a nuget package or just link this assembly from any application thta is NancyFX based rest api we will get automatically the new endpoint '/version'.

To make it useful let's try to return the version of the service which host our module. It's a bit complicated, after checking different posts on stack overflow and other resources I didn't find anything that works for me. So we created a new assembly attribute and a new exception:


[AttributeUsage(AttributeTargets.Assembly)]
class RootAssemblyAttribute : Attribute
{
}




public class RootAssemblyNotFoundException : Exception
{
    public RootAssemblyNotFoundException()
    {
    }

    public RootAssemblyNotFoundException(string message) : base(message)
    {
    }

    public RootAssemblyNotFoundException(string message, Exception innerException) : base(message, innerException)
    {
    }

    protected RootAssemblyNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context)
    {
    }
}

I'ts just a way to mark assembly as one that represents rest api and contains rest api version.
On order to make it work we need to change AssemblyInfo.cs file of main assembly and add in that file new row:


[assembly: RootAssemblyAttribute]

Now we need to adjust a bit VersionModule:


public VersionModule() : base("/version")
{
    Get["/"] = _ =>
    {
        var rootAssembly = AppDomain.CurrentDomain.GetAssemblies()
            .FirstOrDefault(a => a.GetCustomAttributes(typeof (RootAssemblyAttribute), false).Any());

        if (rootAssembly == null)
        {
            return new RootAssemblyNotFoundException("One of assemblies has to be marked as Root assembly");
        }

        return Response.AsJson(new {Verson = rootAssembly.GetName().Version});
    };
}

Sunday, October 12, 2014

Compare arrays in powershell using Pester

Recently I had to create deployment scripts using powershell. At some point our team realised that we need some way to test behavior of our scripts. After googling and comparing different libraries we decided to go with pester. It fits our needs and is actively supported, plus it's really easy to integrate into CI server.

Let's take a look at very simple examples.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Describe 'Pester demo' {
 Context 'Basic math' {   

  It 'Should add 2 numbers' {
   $sum = 2 + 7

   $sum | Should Be 9
  }

  It 'Should deduct 2 numbers' {
   $sum = 10 - 3

   $sum | Should Be 7
  }
 }
}

Nothing special, assertions work as pipeline functions. I personally like that library, it supports different kind of assertions, the only problem that I had is how to assert arrays equality.
Let's try to comapre two arrays.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Describe 'Compare arrays' {
 Context 'There are 2 arrays' {
  $array1 = 1, 2, 3
  $array2 = 1, 2, 3

  It 'Should be green' {
   $array1 | Should Be $array2
  }
 }
}

Output







Looks ok, let's break the test.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Describe 'Compare arrays' {
 Context 'There are 2 arrays' {
  $array1 = 1, 2, 4
  $array2 = 1, 2, 3

  It 'Should be green' {
   $array1 | Should Be $array2
  }
 }
}

Output



It's not really clear  what is really broken.
Let's see how we can improve it:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Describe 'Compare arrays' {
 Context 'There are 2 arrays' {
  $array1 = 1, 2, 4
  $array2 = 1, 2, 3

  $arrayStr1 = $array1 -join ', '
  $arrayStr2 = $array2 -join ', '

  It 'Should be green' {
   $arrayStr1 | Should Be $arrayStr2
  }
 }
}

Output













Now it makes more sense.

Sunday, August 17, 2014

A public action method 'X' was not found on controller 'Y'

Recently I've met a very strange exception in my application. A public action method 'X' was not found on controller 'Y'. After googling for a while I found that it can be when your action is marked with HttpPost or HttpGet attribute, but http method of you request doesn't fit to it. In my case my method action was marked as HttpPost and I was pretty sure that I use POST method in my request. Then I used debugger and found that HttpContext.Request.HttpMethod is equals to "GET". It was really strange. It actually means that something changes my request. I went to Web.config and found something really interesting – UrlRewrite module and bunch of rules. My request was affected by one of them, that led to redirect, and because of redirect it changed original POST http method to GET.

Sunday, July 27, 2014

Build configurations cleanup

Currently I'm working on a big project where we have a lot of legacy code. The big problem was build configurations. There were around 8 or 10 different configurations, of cause DEBUG and RELEASE, but besides we had QA, Production, Integration and Staging. Problem is that we had also some garbage, some old configurations like Demo1, Demo2, Test and something else. Another problem is that some projects were configured as Any CPU other as x86. We had real mess in csproj and sln files.
To normalize everything I created small tool. Basically we need to do two things:

1. Remove all build configurations from csproj files and add there only those that we need.
2. Remove all build mappings from sln files and created them from scratch.

Let's talk about the first step. csproj-file is simple xml file. So what we need is just open xml file, remove some nodes and add another nodes. Here is the part of the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void CureProject(string projectPath, IEnumerable<string> buildConfigurations)
{
    var doc = XDocument.Load(projectPath);

    var ns = doc.Root.GetDefaultNamespace();

    var mainPropertyGroup =
        doc.Root.Elements()
            .Single(
                x =>
                    x.Name.LocalName == "PropertyGroup" &&
                    x.Elements().Any(sx => sx.Name.LocalName == "OutputType"));

    doc.Root.Elements()
        .Where(
            x =>
                x.Name.LocalName == "PropertyGroup" && x.HasAttributes &&
                x.Attributes().Any(atr => atr.Name.LocalName == "Condition"))
        .ToList()
        .ForEach(x => x.Remove());

    var isWeb = doc.Root.ToString().Contains("WebProjectProperties");

    foreach (var buildConfiguration in buildConfigurations)
    {
        if (buildConfiguration == "Debug" || buildConfiguration == "Integration")
        {
            mainPropertyGroup.AddAfterSelf(CreateDebugBasedBuildConfiguration(ns, buildConfiguration, isWeb));
        }
        else
        {
            mainPropertyGroup.AddAfterSelf(CreateReleaseBasedBuildConfiguration(ns, buildConfiguration, isWeb));
        }
    }

    doc.Save(projectPath);
}

Let's discuss what we do here. Each csproj file contains several PropertyGroup nodes. One of them is main one or a default one. It has no conditions. Here's an example:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{449C9CCD-35FC-4D38-932E-6243C4517090}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>SolutionNormalizer</RootNamespace>
    <AssemblyName>SolutionNormalizer</AssemblyName>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
</PropertyGroup>

So we have 2 nodes here. First one is what I call main one, witout any conditions. It has improtant info like project type or target framework. The second node has a condition. And thta is very important. We actualy take main one and then, if codition is satisfied, we take additional node. Then we add or overwrite some properties. It mean tha if configuration is Debug and target platform is AnyCPU then we take additonal properties from tha section as DebugType or OutputPath. So we want to keep a main node and remove the rest. That is what does our code in lines 14-20. Then we need to if thta is a web project. We will need it later. So don't care about it right now. Thaen we have a loop through the collection of build configurations that we want to add. Inside fe have a condition.

if (buildConfiguration == "Debug" || buildConfiguration == "Integration")

It's a bit haky...what I actually want here is to spleet all build configurations into 2 categories: based on debug configuration and based on release configuration. The difference is not so big, we just set some properties a bit differently. We set differently DebugSymbols and Optimization. We will take a look mode deeply. Let's just take a look how we create debug based nodes and release based nodes:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private XNode CreateDebugBasedBuildConfiguration(XNamespace ns, string buildConfiguration, bool isWeb)
{
    var result = new XElement(ns + "PropertyGroup",
        new XAttribute("Condition",
            string.Format(" '$(Configuration)|$(Platform)' == '{0}|AnyCPU' ", buildConfiguration)),
        new XElement(ns + "DebugSymbols", "true"),
        new XElement(ns + "DebugType", "full"),
        new XElement(ns + "Optimize", "false"),
        new XElement(ns + "OutputPath", isWeb ? @"bin\" : string.Format("bin\\{0}\\", buildConfiguration)),
        new XElement(ns + "DefineConstants", "DEBUG;TRACE"),
        new XElement(ns + "ErrorReport", "prompt"),
        new XElement(ns + "WarningLevel", "4")
        );

    return result;
}

private XNode CreateReleaseBasedBuildConfiguration(XNamespace ns, string buildConfiguration, bool isWeb)
{
    var result = new XElement(ns + "PropertyGroup",
        new XAttribute("Condition",
            string.Format(" '$(Configuration)|$(Platform)' == '{0}|AnyCPU' ", buildConfiguration)),
        new XElement(ns + "DebugType", "pdbonly"),
        new XElement(ns + "Optimize", "true"),
        new XElement(ns + "OutputPath", isWeb ? @"bin\" : string.Format("bin\\{0}\\", buildConfiguration)),
        new XElement(ns + "DefineConstants", "TRACE"),
        new XElement(ns + "ErrorReport", "prompt"),
        new XElement(ns + "WarningLevel", "4")
        );

    return result;

The difference is not so big as you can see. As I wrote above we set a bit differently DebugType and Optimize properties. Now we have to discuss why do we need that isWeb parameter. Difference is that noraly build path cotains buildconfiguration name, like bin\Debug or bin\Release. But it's not relevant for web applications. Important thing is the xml namespace that we retriev from csproj file and then use to create the nodes. Wthout that namespace output file will look differetly, but we want to keep everything clean and keep everything as like it was generated by visual studio.

Now we should take a look what should we do with sln files. Here is the code that does all magic.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
private void CureSolution(string solutionPath, IEnumerable<Tuple<string, string>> mappings)
{
    var solution = SolutionParser.Parse(solutionPath);

    var vsFolderGuid = new Guid("2150e333-8fdc-42a3-9474-1a3956d46de8");

    var projects = solution.Projects.Where(p => p.TypeGuid != vsFolderGuid).ToList();

    var projectGuids = projects.Select(p => p.Guid).ToList();

    var slnContent = File.ReadAllText(solutionPath);

    var projectConfigurationRegex =
            new Regex(
                @"GlobalSection\(ProjectConfigurationPlatforms\) = postSolution.*?EndGlobalSection",
                RegexOptions.Singleline);            

    var newContent = CreateProjectConfigurationPlatforms(projectGuids, mappings);

    var result = projectConfigurationRegex.Replace(slnContent, newContent);

    newContent = CreateSolutionConfigurationPlatforms(mappings);

    var solutionConfigurationRegex =
            new Regex(
                @"GlobalSection\(SolutionConfigurationPlatforms\) = preSolution.*?EndGlobalSection",
                RegexOptions.Singleline);

    result = solutionConfigurationRegex.Replace(result, newContent);

    File.WriteAllText(solutionPath, result);
}

private string CreateSolutionConfigurationPlatforms(IEnumerable<Tuple<string, string>> mappings)
{
    var sb = new StringBuilder();

    sb.Append("GlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n");

    foreach (var mapping in mappings)
    {
        sb.AppendFormat("\t\t{0}|Any CPU = {1}|Any CPU\r\n", mapping.Item1, mapping.Item1);
    }

    sb.Append("\tEndGlobalSection");

    return sb.ToString();
}

private string CreateProjectConfigurationPlatforms(IEnumerable<Guid> projectGuids,
    IEnumerable<Tuple<string, string>> mappings)
{
    var sb = new StringBuilder();

    sb.Append("GlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n");

    foreach (var projectGuid in projectGuids)
    {
        foreach (var mapping in mappings)
        {
            var projectFormatedGuid = projectGuid.ToString("B");

            sb.AppendFormat("\t\t{0}.{1}|Any CPU.ActiveCfg = {2}|Any CPU\r\n",
                projectFormatedGuid.ToUpper(), mapping.Item1, mapping.Item2);
            sb.AppendFormat("\t\t{0}.{1}|Any CPU.Build.0 = {2}|Any CPU\r\n", projectFormatedGuid.ToUpper(),
                mapping.Item1, mapping.Item2);
        }
    }

    sb.Append("\tEndGlobalSection");

    return sb.ToString();
}

It's definitely not the best code you've ever seen, it's more like a working prototype. Later I'm going to ckean it up and publish as a console tool. But it works pretty fine and does what I need.
We should star from short intro to what sln file is. It's not an xml file. It's more or less plain text devided in couple sections. At the begining of that file there is a list of all projects linked in that solution. Bellow there are some global sections. We need only two of them. One fefines a list of all available build configurations, another one defines how to map those configurations to configurations that are defined in csproj files.

Here is an example how looks definition of available configurations in solution file:

1
2
3
4
5
GlobalSection(SolutionConfigurationPlatforms) = preSolution
 Debug|Any CPU = Debug|Any CPU
 Release|Any CPU = Release|Any CPU
 QA|Any CPU = QA|Any CPU
EndGlobalSection

It's really straight forward approach. Nothing interesting. It means tha we have 3 configurations available in our solution.
Second scectoins maps solution build configurations to project build configurations. We can do it as one to one mappings or as many to many. Here is an example how we can map one to one.

1
2
3
4
5
6
7
8
GlobalSection(ProjectConfigurationPlatforms) = postSolution
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.Build.0 = Debug|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.ActiveCfg = Release|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.Build.0 = Release|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.ActiveCfg = QA|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.Build.0 = QA|Any CPU
EndGlobalSection

It means that we have 3 build configurations defined on solution level and 3 configurations defined on project level. It menas that project file should contains 3 conditional PropertyGroup nodes.
Bellow is an example how we can map many to many.


1
2
3
4
5
6
7
8
GlobalSection(ProjectConfigurationPlatforms) = postSolution
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.Build.0 = Debug|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.ActiveCfg = Release|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.Build.0 = Release|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.ActiveCfg = Release|Any CPU
 {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection

It means that we have only 2 configurations on project level: debug and release. It means that we have 2 conditional PropertyGroup nodes in csproj file. But we still have 3 build configurations on solution level. We just define that in case of QA it should treat it as it were Release configuation.
You might ask why do I need all that stuff. So in my case there are about 20 sln files and 296 csproj files.And everytime it's diferrent...some solution have more build configurations some less, sometimes it's mpped one to one, sometimes many to many, we just want to keep to same strategy everywhere. Another reason that latter we want to have only Debug and Release build configuraions instead of 8 that we have now. With code that we have seen above it's realy easy. I forget to show what mappings actualy are. So bellow is usage of CureSolution method.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var mappings = new[]
    {
        Tuple.Create("Debug", "Debug"),
        Tuple.Create("Integration", "Debug"),
        Tuple.Create("Release", "Release"),
        Tuple.Create("QA", "Release"),
        Tuple.Create("Production", "Release"),
        Tuple.Create("Staging", "Release")
    };            

foreach (var slnPath in solutions)
{
    CureSolution(slnPath, mappings);
}

In this particular case we map Debug and Integration as debug on project level, and the rest as release configurations.

Last things. I forgot to mention how I parse solution file and get projecs guid's. To do that I use nuget package Onion.SolutionParser. Anothe things is variable vsFolderGuid. Thing is that if solution contains some folder, those folder are treated also like projects with some special project type, we can filter them out by type guid. We have othing to do with solution folders when we do something with build configurations.