{ Copyright (C) 2008 Darius Blaszijk This source is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. A copy of the GNU General Public License is available on the World Wide Web at . You can also obtain it by writing to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. } unit SVNLogForm; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Forms, Dialogs, Controls, FileUtil, LazFileUtils, ComCtrls, StdCtrls, ButtonPanel, ExtCtrls, Spin, DOM, Menus, LCLProc, LazIDEIntf, fgl; type TActionItem = record Action: string; Path: string; CopyPath: string; CopyRev: string; end; { TSVNLogItem } TSVNLogItem = class(TObject) FAuthor: string; FCount: integer; FDate: TDateTime; FMsg: string; FRevision: integer; FAction: array of TActionItem; private function GetAction(Index: Integer): TActionItem; public constructor Create; destructor Destroy; override; function GetActionPointer(Index: Integer): Pointer; procedure AddAction(AActionItem: TActionItem); property Action[Index: Integer]: TActionItem read GetAction; property Count: integer read FCount write FCount; property Revision: integer read FRevision write FRevision; property Author: string read FAuthor write FAuthor; property Date: TDateTime read FDate write FDate; property Msg: string read FMsg write FMsg; end; TSVNLogList = specialize TFPGObjectList; { TSVNLogFrm } TSVNLogFrm = class(TForm) ImageList: TImageList; mnuOpenCurent: TMenuItem; mnuOpenPrevRevision: TMenuItem; mnuOpenRevision: TMenuItem; mnuShowDiff: TMenuItem; SVNActionsPopupMenu: TPopupMenu; RefreshButton: TButton; ButtonPanel: TButtonPanel; Label1: TLabel; LogListView: TListView; SVNActionsListView: TListView; SVNLogMsgMemo: TMemo; SVNLogLimit: TSpinEdit; Splitter1: TSplitter; Splitter2: TSplitter; procedure FormClose(Sender: TObject; var CloseAction: TCloseAction); procedure mnuOpenCurentClick(Sender: TObject); procedure mnuOpenPrevRevisionClick(Sender: TObject); procedure mnuOpenRevisionClick(Sender: TObject); procedure mnuShowDiffClick(Sender: TObject); procedure OKButtonClick(Sender: TObject); procedure RefreshButtonClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormShow(Sender: TObject); procedure LogListViewSelectItem(Sender: TObject; Item: TListItem; Selected: Boolean); procedure SVNActionsPopupMenuPopup(Sender: TObject); private FRepositoryPath: string; { private declarations } LogList: TSVNLogList; procedure UpdateLogListView; procedure ChangeCursor(ACursor: TCursor); public { public declarations } procedure Execute({%H-}Data: PtrInt); property RepositoryPath: string read FRepositoryPath write FRepositoryPath; end; procedure ShowSVNLogFrm(ARepoPath: string); var SVNLogFrm: TSVNLogFrm; implementation {$R *.lfm} uses SVNDiffForm, SVNClasses; procedure ShowSVNLogFrm(ARepoPath: string); begin if not Assigned(SVNLogFrm) then SVNLogFrm := TSVNLogFrm.Create(nil); SVNLogFrm.RepositoryPath:=ARepoPath; SVNLogFrm.Show; end; { TSVNLogItem } function TSVNLogItem.GetAction(Index: Integer): TActionItem; begin if (Index < 0) or (Index >= Count) then raise Exception.CreateFmt(rsIndexOutOfBoundsD, [Index]); Result := FAction[Index]; end; function TSVNLogItem.GetActionPointer(Index: Integer): Pointer; begin if (Index < 0) or (Index >= Count) then raise Exception.CreateFmt(rsIndexOutOfBoundsD, [Index]); Result := @FAction[Index]; end; constructor TSVNLogItem.Create; begin //initialize author to unknown, beacuse in anonymous repositories this really happens Author := rsNoAuthor; end; destructor TSVNLogItem.Destroy; begin inherited Destroy; end; procedure TSVNLogItem.AddAction(AActionItem: TActionItem); begin Inc(FCount); SetLength(FAction, Count); FAction[Count - 1] := AActionItem; end; function FindSVNLogItemByRevision(List: TSVNLogList; RevNo: integer): TSVNLogItem; function SearchLinear(List: TSVNLogList; RevNo: integer): TSVNLogItem; var i: integer; begin Result := nil; for i := 0 to List.Count - 1 do if List.Items[i].Revision = RevNo then begin Result := List.Items[i]; exit; end; end; var tmpRev: integer; index: integer; begin Result := nil; tmpRev := TSVNLogItem(List.Items[0]).Revision; //calculate most probable index index := tmpRev - RevNo; if (index < 0) or (index >= List.Count) then //invalid index, so just do a linear search Result := SearchLinear(List, RevNo) else begin if List.Items[index].Revision = RevNo then //found! Result := List.Items[index] else //revision not found on expected location, search linear Result := SearchLinear(List, RevNo); end; end; { TSVNLogFrm } procedure TSVNLogFrm.FormShow(Sender: TObject); begin ChangeCursor(crHourGlass); Caption := Format(rsLazarusSVNLog, [RepositoryPath]); Application.QueueAsyncCall(@Execute, 0); end; procedure TSVNLogFrm.LogListViewSelectItem(Sender: TObject; Item: TListItem; Selected: Boolean); var RevNo: integer; SVNLogItem: TSVNLogItem; i: integer; begin if not Selected then exit; // this event always fires twice, once for select and once for unselect RevNo := StrToInt(Item.Caption); SVNLogItem := FindSVNLogItemByRevision(LogList, RevNo); SVNActionsListView.Visible := False; // BeginUpdate won't help when autosize is true SVNActionsListView.Clear; if Assigned(SVNLogItem) then begin SVNLogMsgMemo.Lines.Text:=SVNLogItem.Msg; for i := 0 to SVNLogItem.Count - 1 do with SVNActionsListView.Items.Add do begin Caption := SVNLogItem.Action[i].Action; SubItems.Add(CreateRelativePath(SVNLogItem.Action[i].Path, RepositoryPath)); SubItems.Add(CreateRelativePath(SVNLogItem.Action[i].CopyPath, RepositoryPath)); SubItems.Add(SVNLogItem.Action[i].CopyRev); Data := SVNLogItem.GetActionPointer(i); end; end else SVNLogMsgMemo.Clear; SVNActionsListView.Visible := True; end; procedure TSVNLogFrm.SVNActionsPopupMenuPopup(Sender: TObject); var P: TPoint; LI: TListItem; begin // make sure the row under the mouse is selected P := SVNActionsListView.ScreenToControl(Mouse.CursorPos); LI := SVNActionsListView.GetItemAt(P.X, P.Y); if LI <> nil then SVNActionsListView.Selected := LI; end; procedure TSVNLogFrm.UpdateLogListView; var i: integer; LogItem : TSVNLogItem; begin LogListView.Clear; for i := 0 to LogList.Count - 1 do with LogListView.Items.Add do begin LogItem := LogList.Items[i]; //revision Caption := IntToStr(LogItem.Revision); //author SubItems.Add(LogItem.Author); //date SubItems.Add(DateTimeToStr(LogItem.Date)); //message SubItems.Add(LogItem.Msg); end; end; procedure TSVNLogFrm.ChangeCursor(ACursor: TCursor); begin LogListView.Cursor:=ACursor; SVNLogMsgMemo.Cursor:=ACursor; SVNActionsListView.Cursor:=ACursor; Self.Cursor:=ACursor; Application.ProcessMessages; end; procedure TSVNLogFrm.RefreshButtonClick(Sender: TObject); begin ChangeCursor(crHourGlass); Execute(0); end; procedure TSVNLogFrm.mnuShowDiffClick(Sender: TObject); var Path: string; Revision: integer; begin if Assigned(SVNActionsListView.Selected) and Assigned(LogListView.Selected) then begin Revision := StrToInt(LogListView.Selected.Caption); Path := TActionItem(SVNActionsListView.Selected.Data^).Path; DebugLn('TSVNLogFrm.mnuShowDiffClick Path=' , Path); if TActionItem(SVNActionsListView.Selected.Data^).Action = 'M' then ShowSVNDiffFrm(Format('-r %d:%d', [Revision - 1, Revision]), Path) else ShowMessage(rsOnlyModifiedItemsCanBeDiffed); end; end; procedure TSVNLogFrm.OKButtonClick(Sender: TObject); begin Close; end; procedure TSVNLogFrm.mnuOpenCurentClick(Sender: TObject); var Path: String; begin if Assigned(SVNActionsListView.Selected) and Assigned(LogListView.Selected) then begin Path := TActionItem(SVNActionsListView.Selected.Data^).Path; if FileExists(Path) then LazarusIDE.DoOpenEditorFile(Path, -1, -1, [ofOnlyIfExists]) else ShowMessage(rsFileNotInWorkingCopyAnymore); end; end; procedure TSVNLogFrm.mnuOpenPrevRevisionClick(Sender: TObject); begin // end; procedure TSVNLogFrm.mnuOpenRevisionClick(Sender: TObject); begin // end; procedure TSVNLogFrm.FormClose(Sender: TObject; var CloseAction: TCloseAction); begin CloseAction := caFree; end; procedure TSVNLogFrm.FormCreate(Sender: TObject); begin LogList := TSVNLogList.Create; SetColumn(LogListView, 0, 75, rsRevision); SetColumn(LogListView, 1, 75, rsAuthor); SetColumn(LogListView, 2, 150, rsDate); SetColumn(LogListView, 3, 200, rsMessage); SetColumn(SVNActionsListView, 0, 50, rsAction); SetColumn(SVNActionsListView, 1, 200, rsPath); SetColumn(SVNActionsListView, 2, 150, rsCopyFromPath); SetColumn(SVNActionsListView, 3, 75, rsRevision); ImageList.AddResourceName(HInstance, 'menu_svn_diff'); mnuShowDiff.Caption := rsShowDiff; mnuOpenCurent.Caption := rsOpenFileInEditor; mnuOpenRevision.Caption := rsOpenThisRevisionInEditor; mnuOpenPrevRevision.Caption := rsOpenPreviousRevisionInEditor; Label1.Caption:=rsShowDiffCountRev; RefreshButton.Caption:=rsRefresh; end; procedure TSVNLogFrm.FormDestroy(Sender: TObject); begin LogList.Free; SVNLogFrm := nil; end; procedure TSVNLogFrm.Execute(Data: PtrInt); var ActionItem: TActionItem; ActionNode: TDOMNode; Doc: TXMLDocument; InfoUrl: String; InfoRoot: String; i: integer; LogItem: TSVNLogItem; Node: TDOMNode; NodeName: string; SubNode: TDOMNode; t: string; tmpNode: TDOMNode; procedure AddItem(Node: TDomNode); begin with LogListView.Items.Add do begin Caption := Node.NodeName; end; end; function AbsPath(APath: String): String; var Prefix: String; PrefixLength: Integer; APathBak: String; begin // svn will always output a path that is relative // to the repository root on the server // it starts with '/trunk/' or '/branches/foo/' // we have already done 'svn info' and this gave // us 'root' and 'url' so we can now use this to // cut off the prefix and then make // it into an absolute path on our harddrive PrefixLength := Length(InfoUrl) - Length(InfoRoot); Prefix := InfoUrl; Delete(Prefix, 1, Length(InfoRoot)); if Pos(Prefix, APath) = 1 then begin APathBak := APath; // first make the path relative to our working copy // by cutting of the prefix (that only exists on the server) Delete(APath, 1, PrefixLength + 1); // now make it an absolute path with our local repository // base path on our harddrive Result := CreateAbsolutePath(APath, RepositoryPath); // never ever return an absolute local path for a // file that does not exist on our harddrive if not FileExists(Result) then Result := InfoRoot + APathBak; end else begin // if it does not have our prefix then it is from // another directory or branch on the server. Result := InfoRoot + APath; end; end; begin // first get 'svn info' because we need the paths 'root' and 'url' // for some path manipulation to gnerate the absolute paths // of the files on our hard drive Doc := ExecuteSvnReturnXml('info --xml "' + RepositoryPath + '"'); try Node := Doc.DocumentElement.FirstChild.FindNode('url'); InfoUrl := Node.TextContent; Node := Doc.DocumentElement.FirstChild.FindNode('repository').FindNode('root'); InfoRoot := Node.TextContent; Doc.Free; Doc := ExecuteSvnReturnXml('log --xml --verbose --limit ' + IntToStr(SVNLogLimit.Value) + ' "' + RepositoryPath + '" --non-interactive'); LogList.Clear; Node := Doc.DocumentElement.FirstChild; if Assigned(Node) then repeat SubNode := Node; LogItem := TSVNLogItem.Create; //revision LogItem.Revision := StrToInt(SubNode.Attributes.Item[0].NodeValue); //action tmpNode := SubNode.FirstChild; while Assigned(tmpNode) do begin NodeName := tmpNode.NodeName; //Author if NodeName = 'author' then LogItem.Author := tmpNode.FirstChild.NodeValue; //Date if NodeName = 'date' then LogItem.Date := ISO8601ToDateTime(tmpNode.FirstChild.NodeValue); //message if NodeName = 'msg' then if Assigned(tmpNode.FirstChild) then LogItem.Msg:=ReplaceLineEndings(tmpNode.FirstChild.NodeValue, LineEnding); ActionNode := tmpNode.FirstChild; if Assigned(ActionNode) and Assigned(ActionNode.Attributes) then repeat ActionItem.CopyRev := ''; ActionItem.CopyPath := ''; //attributes for i := 0 to ActionNode.Attributes.Length-1 do begin t := ActionNode.Attributes.Item[i].NodeName; if t = 'action' then ActionItem.Action := ActionNode.Attributes.Item[i].NodeValue else if t = 'copyfrom-rev' then ActionItem.CopyRev := ActionNode.Attributes.Item[i].NodeValue else if t = 'copyfrom-path' then ActionItem.CopyPath := AbsPath( ActionNode.Attributes.Item[i].NodeValue); end; //paths ActionItem.Path:=AbsPath(ActionNode.FirstChild.NodeValue); LogItem.AddAction(ActionItem); ActionNode := ActionNode.NextSibling; until not Assigned(ActionNode); tmpNode := tmpNode.NextSibling; end; LogList.Add(LogItem); Node := Node.NextSibling; until not Assigned(Node); finally Doc.Free; UpdateLogListView; ChangeCursor(crDefault); end; end; end.