Finding who’s to blame – Plastic SCM annotate

Thursday, April 09, 2009 0 Comments

One of the less known features in Plastic SCM is its ability to track changes on a line basis using the blame (or annotate) command.
I’m going to explain how to use the cm blame | cm annotate command (actually blame and annotate are aliases to the same cm command), and how it can help you identifying changes.
Let’s start with a very simple file with a few lines of code like the one below.

And then let’s start making changes in the main branch and in two new more branches, until we’ve a tree like the one at the following image.

At any given point in time we can run a blame command to a given file and identify where the changes came from. Let’s see how Plastic SCM can identify it looking at the next diagram.

What Plastic does is climb the version tree upwards from the starting point you’ve at your workspace. If you start up at revision 1 on /main, changes will only come from this revision itself or the one immediately up. Something different will be displayed if you start from rev 2 at /main/task001.

The good thing is that the Plastic annotate/blame command is not only restricted to climbing the tree up through the branch, but it can also go through the other branches as soon as there are available paths (created when you merge branches), so properly identifying where changes come from.

After merging the two branches, the new revision 3 at /main, will have contributors not only from its previous revs on the branch, but from the other branches. The interesting point to highlight is that changes are not identified as coming from revs 2 and 3, but from the correct ones in the branches.
Considering Plastic SCM promotes using branches as much as possible, and linking branches to tasks/issues/defects on your favorite project management/issue tracking system, you’ll be able to track where exactly changes came from.
Let’s now go through a step by step example, using the command line, to explain how annotate behaves in a real scenario.


> cm mkwk wk01 .
Workspace wk01 has been correctly created

> cm co .
The selected items are about to be checked out. Please wait ...
Item . was correctly checked out

> cm add hostname.cs
The selected items are about to be added. Please wait ...
Item hostname.cs was correctly added

> cm ci hostname.cs . -c="initial commit"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Checking in d:\data\code\testwkspaces\wk01 ... Done
Created changeset cs:1@br:/main@rep:default@repserver:BEARDTONGUE:8084


Let's create a couple of smart branches from the CLI


> cm mkbr br:/main/task001

> cm sbb br:/main/task001 cs:1
A new base has been set for the branch br:/main/task001.
A checkpoint changeset cs:2@rep:default@repserver:localhost:8084 has been created

> cm mkbr br:/main/task002

> cm sbb br:/main/task002 cs:1
A new base has been set for the branch br:/main/task002.
A checkpoint changeset cs:3@rep:default@repserver:localhost:8084 has been created

And now make a change on the main branch.

I'll be using a co/ci cycle, but as you'll see below, you can follow a commit-only cycle with Plastic too.


> cm co hostname.cs
The selected items are about to be checked out. Please wait ...
Item hostname.cs was correctly checked out

> // edit hostname.cs

> cm ci hostname.cs -c="class name changed"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:4@br:/main@rep:default@repserver:BEARDTONGUE:8084


Now switch to task001.


> cm stb br:/main/task001
> cm co hostname.cs
The selected items are about to be checked out. Please wait ...
Item hostname.cs was correctly checked out
> // edit hostname.cs
> cm ci hostname.cs -c="variable name changed"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:5@br:/main/task001@rep:default@repserver:BEARDTONGUE:8084


And now I'll show a ci only cycle


> // edit hostname.cs
> cm ci hostname.cs -c="another var name changed"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:6@br:/main/task001@rep:default@repserver:BEARDTONGUE:8084

> // edit hostname.cs

> cm ci hostname.cs -c="more info printed"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:7@br:/main/task001@rep:default@repserver:BEARDTONGUE:8084


Now onto task002


> cm stb br:/main/task002
> // edit hostname.cs
> cm ci hostname.cs -c="usage controlled"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:8@br:/main/task002@rep:default@repserver:BEARDTONGUE:8084
> // edit hostname.cs
> cm ci hostname.cs -c="usage with info"
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:9@br:/main/task002@rep:default@repserver:BEARDTONGUE:8084

Let's go back to br:/main and run the annotate before merging.


> cm stb br:/main

> cm blame hostname.cs
pablo br:/main#0 using System;
pablo br:/main#0 using System.Net;
pablo br:/main#0
pablo br:/main#0 namespace gethostname
pablo br:/main#0 {
pablo br:/main#1 class GetHostNameTestProgram
pablo br:/main#0 {
pablo br:/main#0 [STAThread]
pablo br:/main#0 static void Main(string[] args)
pablo br:/main#0 {
pablo br:/main#0 string howtogeek = args[0];
pablo br:/main#0 IPHostEntry hentry = Dns.GetHostByName(howtogeek);
pablo br:/main#0
pablo br:/main#0 foreach (IPAddress theaddress in hentry.AddressList)
pablo br:/main#0 {
pablo br:/main#0 Console.WriteLine(theaddress.ToString());
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }


And now on the branches:


> cm stb br:/main/task001
> cm blame hostname.cs
pablo br:/main#0 using System;
pablo br:/main#0 using System.Net;
pablo br:/main#0
pablo br:/main#0 namespace gethostname
pablo br:/main#0 {
pablo br:/main#0 class Class1
pablo br:/main#0 {
pablo br:/main#0 [STAThread]
pablo br:/main#0 static void Main(string[] args)
pablo br:/main#0 {
pablo br:/main/task001#0 string hostName = args[0];
pablo br:/main/task001#0 IPHostEntry hentry = Dns.GetHostByName(hostName);
pablo br:/main#0
pablo br:/main/task001#2 Console.WriteLine("Addresses: {0}", hostName);
pablo br:/main/task001#1 foreach (IPAddress address in hentry.AddressList)
pablo br:/main#0 {
pablo br:/main/task001#1 Console.WriteLine(address.ToString());
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }

> cm stb br:/main/task002

> cm annotate hostname.cs
pablo br:/main#0 using System;
pablo br:/main#0 using System.Net;
pablo br:/main#0
pablo br:/main#0 namespace gethostname
pablo br:/main#0 {
pablo br:/main#0 class Class1
pablo br:/main#0 {
pablo br:/main#0 [STAThread]
pablo br:/main#0 static void Main(string[] args)
pablo br:/main#0 {
pablo br:/main/task002#0 if( args.Lenght == 0 )
pablo br:/main/task002#0 {
pablo br:/main/task002#1 Console.WriteLine("usage: hostname name");
pablo br:/main/task002#1 Console.WriteLine("\tname of your host");
pablo br:/main/task002#0 return;
pablo br:/main/task002#0 }
pablo br:/main/task002#0
pablo br:/main#0 string howtogeek = args[0];
pablo br:/main#0 IPHostEntry hentry = Dns.GetHostByName(howtogeek);
pablo br:/main#0
pablo br:/main#0 foreach (IPAddress theaddress in hentry.AddressList)
pablo br:/main#0 {
pablo br:/main#0 Console.WriteLine(theaddress.ToString());
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }

Let's merge the branches back to main:


> cm stb br:/main
Plastic is updating your workspace. Wait a moment, please ...
Copied hostname.cs

> cm merge br:/main/task001 --merge
Merge needed on item hostname.cs
from br:/main/task001#2
to br:/main#1 base br:/main#0.
Changed by both contributors.
Merging hostname.cs
Merge done

> cm ci hostname.cs
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:10@br:/main@rep:default@repserver:BEARDTONGUE:8084


And blame:


> cm blame hostname.cs
pablo br:/main#0 using System;
pablo br:/main#0 using System.Net;
pablo br:/main#0
pablo br:/main#0 namespace gethostname
pablo br:/main#0 {
pablo br:/main#1 class GetHostNameTestProgram
pablo br:/main#0 {
pablo br:/main#0 [STAThread]
pablo br:/main#0 static void Main(string[] args)
pablo br:/main#0 {
pablo br:/main/task001#0 string hostName = args[0];
pablo br:/main/task001#0 IPHostEntry hentry = Dns.GetHostByName(hostName);
pablo br:/main#0
pablo br:/main/task001#2 Console.WriteLine("Addresses: {0}", hostName);
pablo br:/main/task001#1 foreach (IPAddress address in hentry.AddressList)
pablo br:/main#0 {
pablo br:/main/task001#1 Console.WriteLine(address.ToString());
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }


Second branch:


> cm merge br:/main/task002 --merge
Merge needed on item hostname.cs
from br:/main/task002#1
to br:/main#2 base br:/main#0.
Changed by both contributors.
Merging hostname.cs
Merge done

> cm ci hostname.cs
The selected items are about to be checked in. Please wait ...
Checking in hostname.cs ... Done
Created changeset cs:11@br:/main@rep:default@repserver:BEARDTONGUE:8084

> cm blame hostname.cs
pablo br:/main#0 using System;
pablo br:/main#0 using System.Net;
pablo br:/main#0
pablo br:/main#0 namespace gethostname
pablo br:/main#0 {
pablo br:/main#1 class GetHostNameTestProgram
pablo br:/main#0 {
pablo br:/main#0 [STAThread]
pablo br:/main#0 static void Main(string[] args)
pablo br:/main#0 {
pablo br:/main/task002#0 if( args.Lenght == 0 )
pablo br:/main/task002#0 {
pablo br:/main/task002#1 Console.WriteLine("usage: hostname name");
pablo br:/main/task002#1 Console.WriteLine("\t name of your host");
pablo br:/main/task002#0 return;
pablo br:/main/task002#0 }
pablo br:/main/task002#0
pablo br:/main/task001#0 string hostName = args[0];
pablo br:/main/task001#0 IPHostEntry hentry = Dns.GetHostByName(hostName);
pablo br:/main#0
pablo br:/main/task001#2 Console.WriteLine("Addresses: {0}", hostName);
pablo br:/main/task001#1 foreach (IPAddress address in hentry.AddressList)
pablo br:/main#0 {
pablo br:/main/task001#1 Console.WriteLine(address.ToString());
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }
pablo br:/main#0 }

How does the vtree look?


And finally a graphic explaining the annotate I just showed above.



It's also possible to decorate each line with the comment:


>cm blame hostname.cs --format="'{comment, -24}'{content}"
'initial commit 'using System;
'initial commit 'using System.Net;
'initial commit '
'initial commit 'namespace gethostname
'initial commit '{
'class name changed ' class GetHostNameTestProgram
'initial commit ' {
'initial commit ' [STAThread]
'initial commit ' static void Main(string[] args)
'initial commit ' {
'usage controlled ' if( args.Lenght == 0 )
'usage controlled ' {
'usage with info ' Console.WriteLine("usage: hostname name");
'usage with info ' Console.WriteLine("\t name of your host");
'usage controlled ' return;
'usage controlled ' }
'usage controlled '
'variable name changed ' string hostName = args[0];
'variable name changed ' IPHostEntry hentry = Dns.GetHostByName(hostName);
'initial commit '
'more info printed ' Console.WriteLine("Addresses: {0}", hostName);
'another var name changed' foreach (IPAddress address in hentry.AddressList)
'initial commit ' {
'another var name changed' Console.WriteLine(address.ToString());
'initial commit ' }
'initial commit ' }
'initial commit ' }
'initial commit '}


Enjoy!

0 comentarios: