unit tsvnWizard;

{$R 'icons.res'}

interface

uses ToolsAPI, SysUtils, Windows, Dialogs, Menus, Registry, ShellApi,
    Classes, Controls, Graphics, ImgList, ExtCtrls, ActnList;

const
    SVN_PROJECT_EXPLORER = 0;
    SVN_LOG_PROJECT = 1;
    SVN_LOG_FILE = 2;
    SVN_CHECK_MODIFICATIONS = 3;
    SVN_ADD = 4;
    SVN_UPDATE = 5;
    SVN_COMMIT_PROJECT = 6;
    SVN_COMMIT_MODULE = 7;
    SVN_DIFF = 8;
    SVN_REVERT = 9;
    SVN_REPOSITORY_BROWSER = 10;
    SVN_SETTINGS = 11;
    SVN_ABOUT = 12;
    SVN_VERB_COUNT = 13;

type TTortoiseSVN = class(TNotifierObject, IOTANotifier, IOTAWizard)
private
    timer: TTimer;
    tsvnMenu: TMenuItem;
    TSVNPath: string;
    procedure Tick( sender: TObject );
    procedure TSVNMenuClick( sender: TObject );
    procedure DiffClick( sender: TObject );
    procedure LogClick(Sender : TObject);
    procedure TSVNExec( tsvnCommand: string; path: string = ''; extraParams: string = '' );
    function GetBitmapName(Index: Integer): string;
    function GetVerb(Index: Integer): string;
    function GetVerbState(Index: Integer): Word;
    procedure ExecuteVerb(Index: Integer);
    procedure CreateMenu;
    procedure UpdateAction( sender: TObject );
    procedure ExecuteAction( sender: TObject );
public
    constructor Create;
    destructor Destroy; override;
    function GetIDString: string;
    function GetName: string;
    function GetState: TWizardState;
    procedure Execute;
end;


{$IFNDEF DLL_MODE}

procedure Register;

{$ELSE}

function InitWizard(const BorlandIDEServices: IBorlandIDEServices;
  RegisterProc: TWizardRegisterProc;
  var Terminate: TWizardTerminateProc): Boolean; stdcall;

{$ENDIF}


implementation

function GetCurrentProject: IOTAProject;
var
  ModServices: IOTAModuleServices;
  Module: IOTAModule;
  Project: IOTAProject;
  ProjectGroup: IOTAProjectGroup;
  i: Integer;
begin
  Result := nil;
  ModServices := BorlandIDEServices as IOTAModuleServices;
  if ModServices <> nil then
      for i := 0 to ModServices.ModuleCount - 1 do
      begin
        Module := ModServices.Modules[i];
        if Supports(Module, IOTAProjectGroup, ProjectGroup) then
        begin
          Result := ProjectGroup.ActiveProject;
          Exit;
        end
        else if Supports(Module, IOTAProject, Project) then
        begin // In the case of unbound packages, return the 1st
          if Result = nil then
            Result := Project;
        end;
      end;
end;

function GetCurrentModule: IOTAModule;
var ModServices: IOTAModuleServices;
begin
    Result:= nil;
    ModServices:= BorlandIDEServices as IOTAModuleServices;
    if ModServices <> nil then
        Result:= ModServices.CurrentModule;
end;

function IsCurrentModuleModified: boolean;
var Module: IOTAModule;
    FileEditor: IOTAEditor;
    i: integer;
begin
    Result:= false;
    Module:= GetCurrentModule;
    if Module <> nil then
        for i:= 0 to Module.GetModuleFileCount-1 do begin
        try
            FileEditor:= Module.GetModuleFileEditor(i);
            if (FileEditor <> nil) and FileEditor.Modified then begin
                Result:= true;
                Exit;
            end;
        except
            // hack for C++ Builder 5 OTA known issue: if the unit does not
            // have an associated form, calling GetModuleFileEditor(1) throws
            // access violation; calling GetModuleFileEditor(2) gets the .H file
            // (GetModuleFileCount returns 2, though)
            if (i = 1) and (Module.GetModuleFileCount = 2) then
            try
                FileEditor:= Module.GetModuleFileEditor(2);
                if (FileEditor <> nil) and FileEditor.Modified then begin
                    Result:= true;
                    Exit;
                end;
            except
            end;
        end;
        end;
end;

procedure GetCurrentModuleFileList( fileList: TStrings );
var Module: IOTAModule;
    FileEditor: IOTAEditor;
    i: integer;
begin
    fileList.Clear;
    Module:= GetCurrentModule;
    if Module <> nil then
        for i:= 0 to Module.GetModuleFileCount-1 do begin
        try
            FileEditor:= Module.GetModuleFileEditor(i);
            if FileEditor <> nil then
                fileList.Add( FileEditor.GetFileName );
        except
            // hack for C++ Builder 5 OTA known issue: if the unit does not
            // have an associated form, calling GetModuleFileEditor(1) throws
            // access violation; calling GetModuleFileEditor(2) gets the .H file
            // (GetModuleFileCount returns 2, though)
            if (i = 1) and (Module.GetModuleFileCount = 2) then
            try
                FileEditor:= Module.GetModuleFileEditor(2);
                if FileEditor <> nil then
                    fileList.Add( FileEditor.GetFileName );
            except
            end;
        end;
        end;
end;

procedure GetModifiedItems( itemList: TStrings );
var ModServices: IOTAModuleServices;
    Module: IOTAModule;
    FileEditor: IOTAEditor;
    i, j: integer;
begin
    ModServices := BorlandIDEServices as IOTAModuleServices;
    if ModServices <> nil then begin
        for i:= 0 to ModServices.GetModuleCount-1 do begin
            Module:= ModServices.GetModule(i);
            if Module <> nil then begin
                for j:= 0 to Module.GetModuleFileCount-1 do begin
                try
                    FileEditor:= Module.GetModuleFileEditor(j);
                    if (FileEditor <> nil) and FileEditor.Modified then
                        itemList.Add(Module.FileName);
                except
                    // hack for C++ Builder 5 OTA known issue: if the unit does not
                    // have an associated form, calling GetModuleFileEditor(1) throws
                    // access violation; calling GetModuleFileEditor(2) gets the .H file
                    // (GetModuleFileCount returns 2, though)
                    if (j = 1) and (Module.GetModuleFileCount = 2) then
                    try
                        FileEditor:= Module.GetModuleFileEditor(2);
                        if (FileEditor <> nil) and FileEditor.Modified then
                            itemList.Add(Module.FileName);
                    except
                    end;
                end;
                end;
            end;
        end;
    end;
end;



constructor TTortoiseSVN.Create;
var reg: TRegistry;
// defines for 64-bit registry access, copied from Windows include file
// (older IDE versions won't find them otherwise)
const
    KEY_WOW64_64KEY = $0100;
    KEY_WOW64_32KEY = $0200;
begin
    TSVNPath:= '';
    Reg := TRegistry.Create;
    try
        Reg.RootKey := HKEY_LOCAL_MACHINE;
        Reg.Access := KEY_READ;
        if Reg.OpenKey( '\SOFTWARE\TortoiseSVN', false ) then
            TSVNPath:= Reg.ReadString( 'ProcPath' )
        else begin
            //try 64 bit registry
            Reg.Access := KEY_READ or KEY_WOW64_64KEY;
            if Reg.OpenKey( '\SOFTWARE\TortoiseSVN', false ) then
               TSVNPath:= Reg.ReadString( 'ProcPath' )
            else begin
                //try WOW64 bit registry
                Reg.Access := KEY_READ or KEY_WOW64_32KEY;
                if Reg.OpenKey( '\SOFTWARE\TortoiseSVN', false ) then
                   TSVNPath:= Reg.ReadString( 'ProcPath' )
            end;
        end;
    finally
        Reg.CloseKey;
        Reg.Free;
    end;

    tsvnMenu:= nil;

    // create the add-in menu only if TortoiseSVN was found
    if Length(TSVNPath) > 0 then begin
        timer:= TTimer.create(nil);
        timer.interval:= 200;
        timer.OnTimer:= tick;
        timer.enabled:= true;
    end else
        ShowMessage( 'TortoiseSVN add-in:'#13#10'Cannot retrieve TortoiseSVN installation ' +
            'path from registry. Please make sure it is properly installed.'#13#10 +
            'The add-in will not be available.' );

end;

procedure TTortoiseSVN.Tick( sender: TObject );
var intf: INTAServices;
begin
    if BorlandIDEServices.QueryInterface( INTAServices, intf ) = s_OK then begin
        self.createMenu;
        timer.free;
        timer:= nil;
    end;
end;

procedure TTortoiseSVN.TSVNMenuClick( sender: TObject );
var files: TStringList;
    i: integer;
    diff, log, commitModule, item: TMenuItem;
    module: IOTAModule;
begin
    // update the diff item and submenu; the diff action is handled by the
    // menu item itself, not by the action list
    // the 'log file' item behaves in a similar way

    diff:= tsvnMenu.Items[SVN_DIFF];
    diff.Action:= nil;
    diff.OnClick:= nil;
    diff.Enabled:= false;
    diff.Clear;

    log := tsvnMenu.Items[SVN_LOG_FILE];
    log.Action := nil;
    log.OnClick := nil;
    log.Enabled := false;
    log.Clear();

    files:= TStringList.create;
    GetCurrentModuleFileList(files);
    if files.Count > 0 then begin
        diff.Enabled:= true;
        diff.Caption:= 'Diff';
        log.Enabled := true;
        log.Caption := 'Log file';
        if files.Count > 1 then begin
            for i:= 0 to files.count-1 do begin
                item:= TMenuItem.Create(diff);
                item.Caption:= ExtractFileName( files[i] );
                item.OnClick:= DiffClick;
                item.Tag:= i;
                diff.Add( item );
                item := TMenuItem.Create(log);
                item.Caption := ExtractFileName( files[i] );
                item.OnClick := LogClick;
                item.Tag := i;
                log.Add(item);
            end;
        end else begin  // files.Count = 1
            diff.Caption:= 'Diff ' + ExtractFileName( files[0] );
            diff.OnClick:= DiffClick;
            log.Caption := 'Log ' + ExtractFileName( files[0] );
            log.OnClick := LogClick;
        end;
    end;
    files.free;

    // update the 'commit module' item caption
    module:= GetCurrentModule();
    if module <> nil then begin
        commitModule:= tsvnMenu.Items[SVN_COMMIT_MODULE];
        if module.GetModuleFileCount > 1 then
            commitModule.Caption:= 'Commit ' + ChangeFileExt( ExtractFileName(module.FileName), '' ) + ' module...'
        else
            commitModule.Caption:= 'Commit ' + ExtractFileName(module.FileName) + '...';
    end;

end;

procedure TTortoiseSVN.DiffClick( sender: TObject );
var files: TStringList;
    item: TComponent;
begin
    item:= sender as TComponent;
    files:= TStringList.create;
    GetCurrentModuleFileList(files);
    if files.Count > 1 then
        TSVNExec( 'diff', files[item.Tag], '/notempfile' )
    else if files.Count = 1 then
        TSVNExec( 'diff', files[0], '/notempfile' );
    files.free;
end;

procedure TTortoiseSVN.LogClick(Sender : TObject);
var files : TStringList;
    item  : TComponent;
begin
    item  := Sender as TComponent;
    files := TStringList.Create();
    GetCurrentModuleFileList(files);
    if files.Count > 1 then
        TSVNExec( 'log', files[item.Tag], '/notempfile' )
    else if (files.Count = 1) then
        TSVNExec( 'log', files[0], '/notempfile' );
    files.Free();
end;

procedure TTortoiseSVN.CreateMenu;
var mainMenu: TMainMenu;
    item: TMenuItem;
    i: integer;
    bmp: TBitmap;
    action: TAction;
begin
    if tsvnMenu <> nil then exit;

    tsvnMenu:= TMenuItem.Create(nil);
    tsvnMenu.Caption:= 'TortoiseSVN';
    tsvnMenu.OnClick:= TSVNMenuClick;


    for i:= 0 to SVN_VERB_COUNT-1 do begin

        bmp:= TBitmap.create;
        try
          bmp.LoadFromResourceName( HInstance, getBitmapName(i) );
        except end;

        action:= TAction.Create(nil);
        action.ActionList:= (BorlandIDEServices as INTAServices).ActionList;
        action.Caption:= getVerb(i);
        action.Hint:= getVerb(i);
        if (bmp.Width = 16) and (bmp.height = 16) then
            action.ImageIndex:= (BorlandIDEServices as INTAServices).AddMasked( bmp, clBlack );
        bmp.free;
        action.OnUpdate:= updateAction;
        action.OnExecute:= executeAction;
        action.Tag:= i;

        item:= TMenuItem.Create( tsvnMenu );
        item.action:= action;

        tsvnMenu.add( item );
    end;

    mainMenu:= (BorlandIDEServices as INTAServices).MainMenu;
    mainMenu.Items.Insert( mainMenu.Items.Count-1, tsvnMenu );
end;

destructor TTortoiseSVN.Destroy;
begin
    if tsvnMenu <> nil then begin
        tsvnMenu.free;
    end;
    inherited;
end;

function TTortoiseSVN.GetBitmapName(Index: Integer): string;
begin
    case index of
        SVN_PROJECT_EXPLORER:
            Result:= 'explorer';
        SVN_LOG_PROJECT,
        SVN_LOG_FILE:
            Result:= 'log';
        SVN_CHECK_MODIFICATIONS:
            Result:= 'check';
        SVN_ADD:
            Result:= 'add';
        SVN_UPDATE:
            Result:= 'update';
        SVN_COMMIT_PROJECT:
            Result:= 'commit';
        SVN_COMMIT_MODULE:
            Result:= 'commit';
        SVN_DIFF:
            Result:= 'diff';
        SVN_REVERT:
            Result:= 'revert';
        SVN_REPOSITORY_BROWSER:
            Result:= 'repository';
        SVN_SETTINGS:
            Result:= 'settings';
        SVN_ABOUT:
            Result:= 'about';
    end;
end;

function TTortoiseSVN.GetVerb(Index: Integer): string;
begin
    case index of
        SVN_PROJECT_EXPLORER:
            Result:= '&Project explorer...';
        SVN_LOG_PROJECT:
            Result:= '&Log project...';
        SVN_LOG_FILE:
            Result := 'Log &file...';
        SVN_CHECK_MODIFICATIONS:
            Result:= 'Check &modifications...';
        SVN_ADD:
            Result:= '&Add...';
        SVN_UPDATE:
            Result:= '&Update to revision...';
        SVN_COMMIT_PROJECT:
            Result:= '&Commit project...';
        SVN_COMMIT_MODULE:
            Result:= '&Commit module...';
        SVN_DIFF:
            Result:= '&Diff...';
        SVN_REVERT:
            Result:= '&Revert...';
        SVN_REPOSITORY_BROWSER:
            Result:= 'Repository &browser...';
        SVN_SETTINGS:
            Result:= '&Settings...';
        SVN_ABOUT:
            Result:= '&About...';
    end;
end;

const vsEnabled = 1;

function TTortoiseSVN.GetVerbState(Index: Integer): Word;
begin
    Result:= 0;
    case index of
        SVN_PROJECT_EXPLORER:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_LOG_PROJECT:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_LOG_FILE:
            // this verb state is updated by the menu itself
            ;
        SVN_CHECK_MODIFICATIONS:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_ADD:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_UPDATE:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_COMMIT_PROJECT:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_COMMIT_MODULE:
            if GetCurrentModule <> nil then
                Result:= vsEnabled;
        SVN_DIFF:
            // this verb state is updated by the menu itself
            ;
        SVN_REVERT:
            if GetCurrentProject <> nil then
                Result:= vsEnabled;
        SVN_REPOSITORY_BROWSER:
            Result:= vsEnabled;
        SVN_SETTINGS:
            Result:= vsEnabled;
        SVN_ABOUT:
            Result:= vsEnabled;
    end;
end;

procedure TTortoiseSVN.TSVNExec( tsvnCommand: string; path: string = ''; extraParams: string = '' );
var cmdLine: string;
    result: HINST;
begin
    cmdLine:= Format( '/command:%s %s', [tsvnCommand, extraParams] );
    if Length(path) > 0 then
        cmdLine:= Format( '%s /path:"%s"', [cmdLine, path] );
    result:= ShellExecute( 0, nil, pchar(TSVNPath), pchar(cmdLine), nil, SW_SHOW );
    if result <= 32 then
        ShowMessageFmt( 'Error while trying to execute %s'#13#10'Please check your TortoiseSVN installation.', [TSVNPath] );
end;

procedure TTortoiseSVN.ExecuteVerb(Index: Integer);
var project: IOTAProject;
    module: IOTAModule;
    itemList: TStringList;
    modifiedItems: boolean;
    modifiedItemsMessage: string;
    i: integer;
    response: Word;
    filesToCommit: string;
begin
    project:= GetCurrentProject();
    module:= GetCurrentModule();
    case index of
        SVN_PROJECT_EXPLORER:
            if project <> nil then
                ShellExecute( 0, 'open', pchar( ExtractFilePath(project.GetFileName) ), '', '', SW_SHOWNORMAL );
        SVN_LOG_PROJECT:
            if project <> nil then
                TSVNExec( 'log', ExtractFilePath(project.GetFileName), '/notempfile' );
        SVN_LOG_FILE:
            // this verb is handled by its menu item
            ;
        SVN_CHECK_MODIFICATIONS:
            if project <> nil then
                TSVNExec( 'repostatus', ExtractFilePath(project.GetFileName), '/notempfile' );
        SVN_ADD:
            if project <> nil then
                TSVNExec( 'add', ExtractFilePath(project.GetFileName), '/notempfile' );
        SVN_UPDATE:
            if project <> nil then begin
                itemList:= TStringList.Create;
                GetModifiedItems(itemList);
                modifiedItems:= itemList.Count > 0;
                if modifiedItems then begin
                    modifiedItemsMessage:= 'The following modules are modified:' + #13#10#13#10;
                    for i:= 0 to itemList.Count-1 do
                        modifiedItemsMessage:= modifiedItemsMessage + '    ' + itemList[i] + #13#10;
                    modifiedItemsMessage:= modifiedItemsMessage + #13#10 + 'Save them before update?';
                end;
                itemList.Free;
                if modifiedItems then begin
                    response:= MessageDlg( modifiedItemsMessage, mtWarning, [mbYes, mbNo, mbCancel], 0 );
                    if response = mrYes then
                        (BorlandIDEServices as IOTAModuleServices).saveAll
                    else if response = mrCancel then
                        Exit;
                end;
                TSVNExec( 'update', ExtractFilePath(project.GetFileName), '/rev /notempfile' );
            end;
        SVN_COMMIT_PROJECT:
            if project <> nil then begin
                itemList:= TStringList.Create;
                GetModifiedItems(itemList);
                modifiedItems:= itemList.Count > 0;
                if modifiedItems then begin
                    modifiedItemsMessage:= 'The following modules are modified:' + #13#10#13#10;
                    for i:= 0 to itemList.Count-1 do
                        modifiedItemsMessage:= modifiedItemsMessage + '    ' + itemList[i] + #13#10;
                    modifiedItemsMessage:= modifiedItemsMessage + #13#10 + 'Save them before commit?';
                end;
                itemList.Free;
                if modifiedItems then begin
                    response:= MessageDlg( modifiedItemsMessage, mtWarning, [mbYes, mbNo, mbCancel], 0 );
                    if response = mrYes then
                        (BorlandIDEServices as IOTAModuleServices).saveAll
                    else if response = mrCancel then
                        Exit;
                end;
                TSVNExec( 'commit', ExtractFilePath(project.GetFileName), '/notempfile' );
            end;
        SVN_COMMIT_MODULE:
            if module <> nil then begin
                if IsCurrentModuleModified then begin
                    if module.GetModuleFileCount > 1 then
                        modifiedItemsMessage:= 'Some of the module files are modified. Save them before commit?'
                    else
                        modifiedItemsMessage:= 'File ' + ExtractFileName(module.GetFileName) + ' is modified. Save it before commit?';
                    response:= MessageDlg( modifiedItemsMessage, mtWarning, [mbYes, mbNo, mbCancel], 0 );
                    if response = mrYes then
                        module.Save( false, true )
                    else if response = mrCancel then
                        Exit;
                end;
                itemList:= TStringList.Create;
                GetCurrentModuleFileList(itemList);
                if itemList.Count > 0 then
                    filesToCommit:= itemList[0];
                for i:= 1 to itemList.Count-1 do
                    filesToCommit:= filesToCommit + '*' + itemList[i];
                itemList.Free;
                TSVNExec( 'commit', filesToCommit, '/notempfile' );
            end;
        SVN_DIFF:
            // this verb is handled by its menu item
            ;
        SVN_REVERT:
            if project <> nil then
                TSVNExec( 'revert', ExtractFilePath(project.GetFileName), '/notempfile' );
        SVN_REPOSITORY_BROWSER:
            if project <> nil then
                TSVNExec( 'repobrowser', ExtractFilePath(project.GetFileName), '/notempfile' )
            else
                TSVNExec( 'repobrowser' );
        SVN_SETTINGS:
            TSVNExec( 'settings' );
        SVN_ABOUT:
            TSVNExec( 'about' );
    end;
end;

procedure TTortoiseSVN.UpdateAction( sender: TObject );
var action: TAction;
begin
    action:= sender as TAction;
    action.Enabled:= getVerbState( action.tag ) = vsEnabled;
end;

procedure TTortoiseSVN.ExecuteAction( sender: TObject );
var action: TAction;
begin
    action:= sender as TAction;
    executeVerb( action.tag );
end;


function TTortoiseSVN.GetIDString: string;
begin
    result:= 'Subversion.TortoiseSVN';
end;

function TTortoiseSVN.GetName: string;
begin
    result:= 'TortoiseSVN add-in';
end;

function TTortoiseSVN.GetState: TWizardState;
begin
    result:= [wsEnabled];
end;

procedure TTortoiseSVN.Execute;
begin
end;



{$IFNDEF DLL_MODE}

procedure Register;
begin
    RegisterPackageWizard(TTortoiseSVN.create);
end;

{$ELSE}

var wizardID: integer;

procedure FinalizeWizard;
var
  WizardServices: IOTAWizardServices;
begin
    Assert(Assigned(BorlandIDEServices));

    WizardServices := BorlandIDEServices as IOTAWizardServices;
    Assert(Assigned(WizardServices));

    WizardServices.RemoveWizard( wizardID );

end;

function InitWizard(const BorlandIDEServices: IBorlandIDEServices;
  RegisterProc: TWizardRegisterProc;
  var Terminate: TWizardTerminateProc): Boolean; stdcall;
var
  WizardServices: IOTAWizardServices;
begin
    Assert(BorlandIDEServices <> nil);
    Assert(ToolsAPI.BorlandIDEServices = BorlandIDEServices);

    Terminate := FinalizeWizard;

    WizardServices := BorlandIDEServices as IOTAWizardServices;
    Assert(Assigned(WizardServices));

    wizardID:= WizardServices.AddWizard(TTortoiseSVN.Create as IOTAWizard);

    result:= wizardID >= 0;
end;


exports
  InitWizard name WizardEntryPoint;

{$ENDIF}



end.

