How to use Perforces P4.NET API for basic tasks

So recently I had a task to rename some files in Perforce. Ok, one or two files is easy, that can be done by hand. But this task required doing around 800 files based off of data from an external source. So I started looking for a .NET API that would allow me to access Perforce using C# rather than use the command line.

Thankfully Perforce has one.

http://www.perforce.com/product/components/apis

The only problem is documentation is basically no good:

http://www.perforce.com/perforce/doc.current/manuals/p4api.net/p4api.net_reference/Index.html

The help docs have one measly page that gives a tutorial on how it works. And that is only for connecting and disconnecting. For most of the rest of the reference guide contains no description of what anything does, nor what anything is, nor how to use it. The docs are basically just a skeleton of a help file. And on every page is this disclaimer: [This is preliminary documentation and is subject to change.]. Indeed it is. The docs are so bare, they are basically useless. Given a function signature that has no description like this:

Syntax

C#
public List<FileSpec> IntegrateFiles(IList<FileSpec> toFiles,FileSpec fromFile,

Options options

)

Parameters

toFiles

Type: System.Collections.Generic.IList(FileSpec)

fromFile

Type: Perforce.P4.FileSpec

options

Type: Perforce.P4.Options

Return Value

 

You basically have to guess how to use this API!

To add insult to the whole comical affair, Perforce did add documentation for this particular method, not the parameters as you see above, but for the method in general. But it was docs for the command line! As if the command line syntax and idioms could possibly apply to C#! In stunned disbelief I had to press forward as this was my only option for accessing Perforce using a .NET API. I had access different perforce API that uses .NET years ago. But it was so old that it wasn’t compatible with .NET 4.0, or the new perforce servers we use now.

So the point about this blog post is not to complain, but to offer a solution about how I figured all this stuff out.

First off I wrote a class to encapsulate the credentials needed to log on to the Perforce Server. It contained data like the server name and port, the username and the client spec:

public class PerforceID
{
	public PerforceID(String serverURI, String user_name, String client_spec)
	{
		mServerURI = serverURI;
		mUserName = user_name;
		mClientSpec = client_spec;
	}
	private String mServerURI;
	public System.String ServerURI
	{
		get { return mServerURI; }
	}
	private String mUserName;
	public System.String UserName
	{
		get { return mUserName; }
	}
	private String mClientSpec;
	public System.String ClientSpec
	{
		get { return mClientSpec; }
	}
}

This instance is constructed with the appropriate data and used later.

I kept all my methods in a static class, with just a few static data members.

How to Connect to a Perforce Server

Obviously connecting to the perforce server was of paramount importance. Thankfully the Perforce documentation did describe how to connect (at least). So here I basically borrow from their documentation, except I am using my own static variables. And as you can see I am using my own PerforceID instance to provide the server address (id.ServerURI), the user name (id.UserName) and the client spec I want to use (id.ClientSpec).

private static Perforce.P4.Connection mPerforceConnection;
private static Perforce.P4.Repository mRepository;
public static void Init(PerforceID id)
{
	// initialize the connection variables
	// note: this is a connection without using a password

	// define the server, repository and connection
	Server server = new Server(new ServerAddress(id.ServerURI));
	mRepository = new Repository(server);
	mPerforceConnection = mRepository.Connection;

	// use the connection varaibles for this connection
	mPerforceConnection.UserName = id.UserName;
	mPerforceConnection.Client = new Client();
	mPerforceConnection.Client.Name = id.ClientSpec;

	// connect to the server
	mPerforceConnection.Connect(null);
}

As you can see above the Repository and Connection instance need to be used later, hence why I grab a hold of them for later use.

How to Disconnect from a Perforce Server

This part is easy (again borrowed from their docs).

public static void UnInit()
{
	if (mPerforceConnection != null)
	{
		mPerforceConnection.Disconnect();
		mPerforceConnection = null;
	}
}

How to open a file for edit

Editing a file is the most basic operation I could hope to do. Unfortunately it was not straight-forward at all. So given a string that contains the full file path, this method will open the file for editing in Perforce.

public static bool Edit(String filename)
{
	Perforce.P4.Options options = new Options();
	mPerforceConnection.Client.EditFiles(options, new FileSpec[] { new FileSpec( new ClientPath( filename) ) });
	return true;
}

It was not my intention to open it for editing in any particular changelist. Thus after calling this method, the file will be opened for editing in the default changelist.

I found that the Perforce.P4.Client class contained a lot of the methods I would need to do various familiar operations. Operations that you could logically imagine a user doing manually such as Adding files to a changelist, deleting files, integrating files, shelving and unshelving, merging, moving, locking files etc…

How to integrate a file

Integration or branching (versus copying) is important because it retains the file history. For this operation you need two arguments: the old and new file name. Both must be the full qualified path names containing the directory and file.

using Perforce.P4;
public static bool Integrate(String old_name, String new_name)
{
	bool result = false;
	try
	{
		var from = new FileSpec(new ClientPath(old_name), VersionSpec.Head);
		var to = new FileSpec(new ClientPath(new_name), VersionSpec.None);

		mPerforceConnection.Client.IntegrateFiles(from, options, to);
		result = true;
	}
	catch (Exception e)
	{
		Console.WriteLine("Unknown exception calling Perforce.");
		Console.Write(e.Message);
		result = false;
	}

	return result;
}
static IntegrateFilesCmdOptions options = new IntegrateFilesCmdOptions(IntegrateFilesCmdFlags.None, -1, 0, null, null, null);

The reason I instantiated the instance of IntegrateFilesCmdOptions outside of the function is because I had to call this function hundreds of times. Hence it didn’t make sense redo that instantiation every time.  Also like all Perforce commands, I wrote inside an exception handler since I was guessing how the API worked. For instance I had to guess what parameters I could pass into my options instance above. This will integrate the files and put them in the default changelist. The last 3 parameters I still have no idea what they do, but passing in null works!

This method works, but the only problem is that calling this 800 times is very slow. After about 30 seconds of this I decided I needed a faster approach. I needed to integrate 800 files in a few seconds.

How to integrate lots of files (fast)

So given the requirement I needed to integrate hundreds of files, here is the fast way to do it. I needed to replace 800 calls to the server with just a few instead. This is done by first creating a branch and then integrating the branch. The branch spec will contain what files get branch to where. And the integrate command will be given the branch spec as its main specification. Therefore basically two commands get sent to the perforce servers instead of a few hundred. The result is spectacularly fast compared to the old way.

At the heart of a branch definition is a mapping of old files to the new files. The old file is always on the left, and the new file is on the right. So given that I want to integrate or branch file A.cpp to a new location and rename it to B.cpp, the mapping would look like this:

//depot/foo/A.cpp       //depot/new/output/B.cpp

How to Create a branch

Given those file specifications, creating a branch spec also requires giving it a name, and specifying the user name:

using Perforce.P4;
public static bool CreateBranch(String branch_id, String username, Dictionary<String,String> fileList)
{
	try
	{
		var view_map = new ViewMap();
		foreach( var pair in fileList )
		{
			var from = new ClientPath(pair.Key);
			var to = new ClientPath(pair.Value);

			view_map.Add(new MapEntry(MapType.Include, from, to));
		}

		var msg = "Created programmatically via the Perforce .NET api. Used for integrating lots of files at once.";
		bool locked = true;
		var branch = new BranchSpec(branch_id, username, DateTime.Now, DateTime.Now, msg, locked, view_map, null, null);
		var created = mRepository.CreateBranchSpec(branch);
		Debug.Assert(created != null);
	}
	catch (Exception e)
	{
		Console.WriteLine("Unknown exception calling Perforce.");
		Console.Write(e.Message);
		return false;
	}

	return true;
}

Here the first argument to the method branch_id is the unique identifying name of the branch. The username argument is required for some reason. The last argument, the Dictionary contains the file mappings from old to new names. In this example above the method returns true if it succeeded and false otherwise. Again I wrapped it in an exception handler because I had to figure out how to use this API on my own by guessing, which I accomplished by a lot of trial and error.

To verify that the branch was created, I opened perforce and browsed the branch specs that were owned by me, and indeed the new branch spec showed up in the list.

How to Integrate a branch spec

Now that my branch spec is defined, I can easily integrate it using just the unique name I gave it when the branch was created:

using Perforce.P4;
public static void IntegrateBranch(String branch_id)
{
	try
	{
		var change_list = -1;
		var max_files = -1;
		var branch_options = new IntegrateFilesCmdOptions(IntegrateFilesCmdFlags.None, change_list, max_files, branch_id, null, null);
		var created = mPerforceConnection.Client.IntegrateFiles(branch_options, null);
		Debug.Assert(created != null);
	}
	catch (Exception e)
	{
		Console.WriteLine("Unknown exception calling Perforce.");
		Console.Write(e.Message);
	}
}

So the two methods above could be called in succession like this:


CreateBranch(“mybranch”, “John_Doe”, ….);
IntegrateBranch(“mybranch”);

		
Advertisements