Thursday, March 24, 2011

A LogParser Input Plugin to Search an SVN Repository

Summary

Below is an example of how to write a custom LogParser input plugin, using SharpSvn to query an svn repository.

Download the LogParser.Svn project here. You will need Visual Studio 2010 Express to build it. Make sure that the build tab in the project properties has "Register for COM interop" checked. You will also need to download the latest build of SharpSvn and update the references in the project.

Any reference to examples refer to the following command line:

LogParser.exe -rtp:-1 -i:COM -iProgID:LogParser.Svn.SvnLogParserInputContext -iCOMParams:startDate=2010-01-01 "Select Count(1), Author From http://my.svn/repo/trunk Group By Author Order By Count(1)"

To start, create a new "Class Library" project in Visual Studio. I've called mine LogParser.Svn.

Next, we need to create copies of the COM interface and enum our code will use:

    public enum FieldType : int
    {
        Integer = 1,
        Real = 2,
        String = 3,
        Timestamp = 4,
        Null = 5,
    };

    public interface ILogParserInputContext
    {
        void OpenInput(string from);
        int GetFieldCount();
        string GetFieldName(int index);
        FieldType GetFieldType(int index);
        bool ReadRecord();
        object GetValue(int index);
        void CloseInput(bool abort);
    }

Now we need a class that implements the interface defined above. Let's start with the OpenInput method. This method will receive a string that is entered in the query given to LogParser that represents the "FROM" statement. Taking the command line above, it would receive a value of "http://my.svn/repo/trunk".

        public void OpenInput(string from)
        {
//            System.Diagnostics.Debugger.Launch();

            _client = new SvnClient();
            _repository = from;

            SvnLogArgs args = new SvnLogArgs();

            if (_start.HasValue)
            {
                if (_end.HasValue)
                {
                    args.Range = new SvnRevisionRange(_start.Value, _end.Value);
                }
                else
                {
                    args.Range = new SvnRevisionRange(new SvnRevision(_start.Value), SvnRevision.Head);
                }
            }
            else if (_end.HasValue)
            {
                args.Range = new SvnRevisionRange(SvnRevision.Zero, new SvnRevision(_end.Value));
            }

            if (_startDate.HasValue)
            {
                if (_endDate.HasValue)
                {
                    args.Range = new SvnRevisionRange(_startDate.Value, _endDate.Value);
                }
                else
                {
                    args.Range = new SvnRevisionRange(new SvnRevision(_startDate.Value), SvnRevision.Head);
                }
            }
            else if (_endDate.HasValue)
            {
                args.Range = new SvnRevisionRange(SvnRevision.Zero, new SvnRevision(_endDate.Value));
            }

            _results = new List();

            _client.Log(new Uri(_repository), args, delegate(object sender, SvnLogEventArgs e)
            {
                if (_splitFiles)
                {
                    foreach (SvnChangeItem i in e.ChangedPaths)
                    {
                        object[] result = new object[6];

                        result[0] = e.Author;
                        result[1] = e.LogMessage;
                        result[2] = e.Revision;
                        result[3] = e.Time;
                        result[4] = i.Path;

                        _results.Add(result);
                    }
                }
                else
                {
                    object[] result = new object[6];

                    result[0] = e.Author;
                    result[1] = e.LogMessage;
                    result[2] = e.Revision;
                    result[3] = e.Time;

                    StringBuilder paths = new StringBuilder();

                    foreach (SvnChangeItem i in e.ChangedPaths)
                    {
                        paths.AppendFormat("{0};", i.Path);
                    }

                    result[4] = paths.ToString();

                    _results.Add(result);
                }

            });
        }

First, the code above creates a new SvnClient object, which we will us to view log messages. Next we create an SvnLogArgs object, and set the revision range we want. Finally, we call SvnClient.Log. In the delegate, we receive each log entry matching our SvnLogArgs object. We then create an array of values and store them in a List. Ideally, we would only prepare to receive a value, and then receive each entry during each call to ReadRecord, but I haven't gotten around to doing that (but it shouldn't be too difficult to change the code to work this way).

The next few methods are very straight forward:

        public int GetFieldCount()
        {
            return 5;
        }

        public string GetFieldName(int index)
        {
            switch (index)
            {
                case 0:
                    return "Author";
                case 1:
                    return "LogMessage";
                case 2:
                    return "Revision";
                case 3:
                    return "Time";
                case 4:
                    return "ChangedPaths";
            }

            return null;
        }

        public FieldType GetFieldType(int index)
        {
            switch (index)
            {
                case 0:
                    return FieldType.String;
                case 1:
                    return FieldType.String;
                case 2:
                    return FieldType.Integer;
                case 3:
                    return FieldType.Timestamp;
                case 4:
                    return FieldType.String;
            }

            return FieldType.Null;
        }

        public bool ReadRecord()
        {
            return ++_rowNumber < _results.Count;
        }

        public object GetValue(int index)
        {
            return _results[_rowNumber][index];
        }

        public void CloseInput(bool abort)
        {
            _client.Dispose();
        }

GetFieldCount returns how many fields your parser can return. GetFieldName returns what the field name is for a given index. GetFieldType returns the type of field for a given index. ReadRecord is where your plugin should advance to the next record, and return true if another record is found. GetValue returns the value of the specified field for the current record. CloseInput is called at the end, and is where you should do any cleanup needed.

You can also create some write-only properties that can be set on the command line using -iCOMParams:prop=value. See the attached code for an example.

I hope this helps people trying to start using SharpSvn or writing their own LogParser input plugin.

2 comments:

  1. What steps are required to make your assembly visible via COM to LogParser? The ancient example that comes with LogParser mentions gacutil and regasm. Your project doesn't have a strong name.

    When I try to duplicate your project with a strong name, using gacutil and regasm, I see my type (ProgID and GUID) in the registry and as in COM, but LogParser doesn't seem to find it. Unlike your project, when I build mine in VS 2010 I get a warning about it not containing any types that can be registered for interop. Not sure if that's the problem or not.

    Any direction on figuring out this last piece would be great, perhaps simply by stating how I should make your assembly available to LogParser (not mentioned in the article).

    Thanks,

    Donnie Hale

    ReplyDelete
  2. Do you have this code in another repository? http://rickbassham.googlecode.com/files/LogParser.Svn.zip is dead.

    Thanks.

    ReplyDelete