lazarus/components/lazsvnpkg/svnclasses.pas
darius ec0192e830 fixed crash when committing filenames containing spaces
fixed crash in datetime conversion when showing "added" files
code refactor for SVNCommitForm and SVNDiffForm, code moved to SVNClasses
changed cursor to crHourGlass when recieving data for SVNCommitForm and SVNDiffForm
implemented Execute method in SVNCommitForm and calling this Asynchroneously

git-svn-id: trunk@17187 -
2008-11-03 10:04:59 +00:00

736 lines
20 KiB
ObjectPascal

{ 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 <http://www.gnu.org/copyleft/gpl.html>. You can also obtain it by writing
to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
MA 02111-1307, USA.
}
unit SVNClasses;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, ComCtrls, FileUtil, XMLCfg, LCLProc, Dialogs, Controls,
XMLRead, DOM, Process, StdCtrls, Forms;
resourcestring
rsAction = 'Action';
rsAdded = 'Added';
rsAuthor = 'Author';
rsCommit = 'Commit';
rsCommitRevision = 'Commit revision';
rsConflict = 'Conflict';
rsCopyFromPath = 'Copy from path';
rsDate = 'Date';
rsDelete = 'Delete';
rsDeleted = 'Deleted';
rsDiffActiveFile = 'Diff active file';
rsEdit = 'Edit';
rsExtension = 'Extension';
rsFileStatus = 'File status';
rsIndexOutOfBoundsD = 'Index out of bounds (%d)';
rsLazarusSVNCommit = 'LazarusSVN Commit';
rsLazarusSVNDiff = '%s - LazarusSVN Diff...';
rsLazarusSVNLog = '%s - LazarusSVN Log...';
rsLazarusSVNUpdate = '%s - LazarusSVN Update...';
rsMerged = 'Merged';
rsMessage = 'Message';
rsNoAuthor = '(no author)';
rsPath = 'Path';
rsProjectFilename = 'Project filename';
rsProjectIsActive = 'Project is active';
rsProjectIsNotActiveInSVNSettingsPleaseActivateFirst = 'Project is not '
+'active in SVN settings, please activate first.';
rsProjectName = 'Project name';
rsPropertyStatus = 'Property status';
rsRepositoryPath = 'Repository path';
rsRevision = 'Revision';
rsSave = 'Save';
rsSettings = 'Settings';
rsShowDiff = 'Show diff';
rsShowLog = 'Show log';
rsSourceFileDoesNotBelongToTheProjectPleaseAddFirst = 'Source file does not '
+'belong to the project. Please add first.';
rsSVNTools = 'SVN tools';
rsUpdate = 'Update';
rsUpdated = 'Updated';
const
READ_BYTES = 2048;
type
TSortDirection = (sdAscending, sdDescending);
{ TSVNSettings }
TSVNSettings = class(TObject)
private
{ private declarations }
XML: TXMLConfig;
function GetActive(Index: integer): boolean;
function GetPath(Index: integer): string;
function GetProjectCount: integer;
function GetRepository(Index: integer): string;
procedure SetActive(Index: integer; const AValue: boolean);
procedure SetPath(Index: integer; const AValue: string);
procedure SetProjectCount(const AValue: integer);
procedure SetRepository(Index: integer; const AValue: string);
public
{ public declarations }
constructor Create(const AFileName: string);
destructor Destroy; override;
function RepositoryByPath(APath: string; var ARepo: string): boolean;
procedure AddProject(APath, ARepo: string; AActive: boolean = False);
procedure UpdateProject(APath, ARepo: string; AActive: boolean);
procedure DeleteProjectByIndex(Index: integer);
property ProjectCount: integer read GetProjectCount write SetProjectCount;
property Path[Index: integer]: string read GetPath write SetPath;
property Repository[Index: integer]: string read GetRepository write SetRepository;
property Active[Index: integer]: boolean read GetActive write SetActive;
end;
TStatusItemName = (siChecked, siPath, siExtension, siPropStatus, siItemStatus,
siRevision, siCommitRevision, siAuthor, siDate);
PSVNStatusItem = ^TSVNStatusItem;
TSVNStatusItem = record
Checked: boolean;
Path: string;
Extension: string;
PropStatus: string;
ItemStatus: string;
Revision: integer;
CommitRevision: integer;
Author: string;
Date: TDate;
end;
{ TSVNStatus }
TSVNStatus = class(TObject)
private
FRepositoryPath: string;
FSortDirection: TSortDirection;
FSortItem: TStatusItemName;
public
List: TFPList;
constructor Create(const ARepoPath: string);
destructor Destroy; override;
property RepositoryPath: string read FRepositoryPath write FrepositoryPath;
procedure Sort(ASortItem: TStatusItemName; ADirection: TSortDirection);
procedure ReverseSort(ASortItem: TStatusItemName);
property SortDirection: TSortDirection read FSortDirection write FSortDirection;
property SortItem: TStatusItemName read FSortItem write FSortItem;
end;
var
SVNSettings: TSVNSettings;
procedure CmdLineToMemo(CmdLine: string; Memo: TMemo);
procedure SetColumn(ListView: TListView; ColNo, DefaultWidth: integer; AName: string; AutoSize: boolean = true);
function SVNExecutable: string;
function ReplaceLineEndings(const s, NewLineEnds: string): string;
function ISO8601ToDateTime(ADateTime: string): TDateTime;
implementation
uses
SVNAddProjectForm;
procedure CmdLineToMemo(CmdLine: string; Memo: TMemo);
var
AProcess: TProcess;
BytesRead: LongInt;
n: LongInt;
M: TMemoryStream;
ProcessMsg: string;
procedure UpdateStringsFromStream(Strings: TStrings; var M: TMemoryStream; var BytesRead: LongInt);
var
s: string;
begin
SetLength(s, BytesRead);
M.Read(s[1], BytesRead);
ProcessMsg := ProcessMsg + ReplaceLineEndings(s, LineEnding);
Strings.Text := ProcessMsg;
M.SetSize(0);
BytesRead:=0;
end;
begin
ProcessMsg := '';
AProcess := TProcess.Create(nil);
AProcess.CommandLine := CmdLine;
debugln('CmdLineToMemo commandline=', AProcess.CommandLine);
AProcess.Options := AProcess.Options + [poUsePipes, poStdErrToOutput];
AProcess.ShowWindow := swoHIDE;
AProcess.Execute;
M := TMemoryStream.Create;
BytesRead := 0;
while AProcess.Running do
begin
// make sure we have room
M.SetSize(BytesRead + READ_BYTES);
// try reading it
n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
if n > 0
then begin
Inc(BytesRead, n);
UpdateStringsFromStream(Memo.Lines, M, BytesRead);
Application.ProcessMessages;
end
else
// no data, wait 100 ms
Sleep(100);
end;
// read last part
repeat
// make sure we have room
M.SetSize(BytesRead + READ_BYTES);
// try reading it
n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
if n > 0
then begin
Inc(BytesRead, n);
UpdateStringsFromStream(Memo.Lines, M, BytesRead);
Application.ProcessMessages;
end;
until n <= 0;
M.SetSize(BytesRead);
UpdateStringsFromStream(Memo.Lines, M, BytesRead);
AProcess.Free;
M.Free;
Memo.Cursor:=crDefault;
end;
procedure SetColumn(ListView: TListView; ColNo, DefaultWidth: integer; AName: string; AutoSize: boolean = true);
begin
ListView.Column[ColNo].Caption:=AName;
ListView.Column[ColNo].AutoSize:=AutoSize;
ListView.Column[ColNo].Width:=DefaultWidth;
end;
function SVNExecutable: string;
begin
//encapsulate with " because of the incompatibility on windows
//when svn in in "Program Files" directory
Result := '"' + FindDefaultExecutablePath('svn') + '"';
end;
function ReplaceLineEndings(const s, NewLineEnds: string): string;
var
p: Integer;
StartPos: LongInt;
begin
Result:=s;
p:=1;
while (p<=length(Result)) do begin
if Result[p] in [#10,#13] then begin
StartPos:=p;
if (p<length(Result))
and (Result[p+1] in [#10,#13]) and (Result[p]<>Result[p+1]) then
inc(p);
Result:=copy(Result,1,StartPos-1)+NewLineEnds+copy(Result,p+1,length(Result));
inc(p,length(NewLineEnds));
end else begin
inc(p);
end;
end;
end;
function ISO8601ToDateTime(ADateTime: string): TDateTime;
var
y, m, d, h, n, s: word;
begin
y := StrToInt(Copy(ADateTime, 1, 4));
m := StrToInt(Copy(ADateTime, 6, 2));
d := StrToInt(Copy(ADateTime, 9, 2));
h := StrToInt(Copy(ADateTime, 12, 2));
n := StrToInt(Copy(ADateTime, 15, 2));
s := StrToInt(Copy(ADateTime, 18, 2));
Result := EncodeDate(y,m,d) + EncodeTime(h,n,s,0);
end;
{ TSVNSettings }
function TSVNSettings.GetActive(Index: integer): boolean;
begin
Result := XML.GetValue('projects/item' + IntToStr(Index) + '/active', False);
end;
function TSVNSettings.GetPath(Index: integer): string;
begin
Result := XML.GetValue('projects/item' + IntToStr(Index) + '/path', '');
end;
function TSVNSettings.GetProjectCount: integer;
begin
Result := XML.GetValue('projects/count', 0);
end;
function TSVNSettings.GetRepository(Index: integer): string;
begin
Result := XML.GetValue('projects/item' + IntToStr(Index) + '/repository', '');
end;
procedure TSVNSettings.SetActive(Index: integer; const AValue: boolean);
begin
XML.SetValue('projects/item' + IntToStr(Index) + '/active', AValue);
end;
procedure TSVNSettings.SetPath(Index: integer; const AValue: string);
begin
XML.SetValue('projects/item' + IntToStr(Index) + '/path', AValue);
end;
procedure TSVNSettings.SetProjectCount(const AValue: integer);
begin
XML.SetValue('projects/count', AValue);
end;
procedure TSVNSettings.SetRepository(Index: integer; const AValue: string);
begin
XML.SetValue('projects/item' + IntToStr(Index) + '/repository', AValue);
end;
constructor TSVNSettings.Create(const AFileName: string);
begin
XML := TXMLConfig.Create(nil);
XML.Filename:=AFileName;
end;
destructor TSVNSettings.Destroy;
begin
XML.Flush;
XML.Free;
inherited Destroy;
end;
procedure TSVNSettings.AddProject(APath, ARepo: string; AActive: boolean);
var
count: integer;
begin
count := ProjectCount;
Inc(Count);
ProjectCount := count;
Path[Count - 1] := APath;
Active[Count - 1] := AActive;
Repository[Count - 1] := ARepo;
end;
procedure TSVNSettings.UpdateProject(APath, ARepo: string; AActive: boolean);
var
count: integer;
i: integer;
begin
debugln('TSVNSettings.UpdateProject searching for project');
count := ProjectCount;
for i := 0 to Count - 1 do
if Path[i] = APath then
begin
Active[i] := AActive;
Repository[i] := ARepo;
exit;
end;
//project not found, so add it as new
debugln('TSVNSettings.UpdateProject project not found adding a new one');
AddProject(APath, ARepo, AActive);
end;
procedure TSVNSettings.DeleteProjectByIndex(Index: integer);
var
i: integer;
count: integer;
begin
count := ProjectCount;
ProjectCount := count - 1;
for i := Index to count - 1 do
begin
Active[i] := Active[i + 1];
Path[i] := Path[i + 1];
Repository[i] := Repository[i + 1];
end;
XML.DeletePath('projects/item' + IntToStr(count - 1) + '/active');
XML.DeletePath('projects/item' + IntToStr(count - 1) + '/path');
XML.DeletePath('projects/item' + IntToStr(count - 1) + '/repository');
end;
function TSVNSettings.RepositoryByPath(APath: string; var ARepo: string
): boolean;
var
count: integer;
i: integer;
begin
debugln('TSVNSettingsFrm.GetRepository APath=' + APath);
count := ProjectCount;
for i := 0 to Count - 1 do
if Path[i] = APath then
begin
Result := Active[i];
ARepo := Repository[i];
exit;
end;
if QuestionDlg('Project not found',
'Current project not in project list. Would you like to add it?',
mtWarning,
[mrYes, mrNo],
0) = mrYes then
begin
ShowSVNAddProjectFrm(APath, ARepo, True);
Result := True;
end;
end;
function SortSelectedAscending(Item1, Item2: Pointer): Integer;
begin
if PSVNStatusItem(Item1)^.Checked > PSVNStatusItem(Item2)^.Checked then
Result := 1
else
if PSVNStatusItem(Item1)^.Checked = PSVNStatusItem(Item2)^.Checked then
Result := 0
else
Result := -1;
end;
function SortSelectedDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortSelectedAscending(Item1, Item2);
end;
function SortPathAscending(Item1, Item2: Pointer): Integer;
begin
Result := CompareText(PSVNStatusItem(Item1)^.Path, PSVNStatusItem(Item2)^.Path);
end;
function SortPathDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPathAscending(Item1, Item2);
end;
function SortExtensionAscending(Item1, Item2: Pointer): Integer;
begin
Result := CompareText(PSVNStatusItem(Item1)^.Extension, PSVNStatusItem(Item2)^.Extension);
end;
function SortExtensionDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortExtensionAscending(Item1, Item2);
end;
function SortItemStatusAscending(Item1, Item2: Pointer): Integer;
begin
Result := CompareText(PSVNStatusItem(Item1)^.ItemStatus, PSVNStatusItem(Item2)^.ItemStatus);
end;
function SortItemStatusDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortItemStatusAscending(Item1, Item2);
end;
function SortPropStatusAscending(Item1, Item2: Pointer): Integer;
begin
Result := CompareText(PSVNStatusItem(Item1)^.PropStatus, PSVNStatusItem(Item2)^.PropStatus);
end;
function SortPropStatusDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPropStatusAscending(Item1, Item2);
end;
function SortPropertyAuthorAscending(Item1, Item2: Pointer): Integer;
begin
Result := CompareText(PSVNStatusItem(Item1)^.Author, PSVNStatusItem(Item2)^.Author);
end;
function SortPropertyAuthorDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPropertyAuthorAscending(Item1, Item2);
end;
function SortPropertyRevisionAscending(Item1, Item2: Pointer): Integer;
begin
if PSVNStatusItem(Item1)^.Revision > PSVNStatusItem(Item2)^.Revision then
Result := 1
else
if PSVNStatusItem(Item1)^.Revision = PSVNStatusItem(Item2)^.Revision then
Result := 0
else
Result := -1;
end;
function SortPropertyRevisionDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPropertyRevisionAscending(Item1, Item2);
end;
function SortPropertyCommitRevisionAscending(Item1, Item2: Pointer): Integer;
begin
if PSVNStatusItem(Item1)^.CommitRevision > PSVNStatusItem(Item2)^.CommitRevision then
Result := 1
else
if PSVNStatusItem(Item1)^.CommitRevision = PSVNStatusItem(Item2)^.CommitRevision then
Result := 0
else
Result := -1;
end;
function SortPropertyCommitRevisionDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPropertyCommitRevisionAscending(Item1, Item2);
end;
function SortPropertyDateAscending(Item1, Item2: Pointer): Integer;
begin
if PSVNStatusItem(Item1)^.Date > PSVNStatusItem(Item2)^.Date then
Result := 1
else
if PSVNStatusItem(Item1)^.Date = PSVNStatusItem(Item2)^.Date then
Result := 0
else
Result := -1;
end;
function SortPropertyDateDescending(Item1, Item2: Pointer): Integer;
begin
Result := -SortPropertyDateAscending(Item1, Item2);
end;
{ TSVNStatus }
constructor TSVNStatus.Create(const ARepoPath: string);
var
ActNode: TDOMNode;
AProcess: TProcess;
BytesRead: LongInt;
Doc: TXMLDocument;
F: LongInt;
i: integer;
ListItem: PSVNStatusItem;
M: TMemoryStream;
n: LongInt;
Node: TDOMNode;
NodeName: string;
NodeValue: string;
Path: string;
SubNode: TDOMNode;
begin
List := TFPList.Create;
RepositoryPath := ARepoPath;
M := TMemoryStream.Create;
BytesRead := 0;
AProcess := TProcess.Create(nil);
AProcess.CommandLine := SVNExecutable + ' stat --verbose --xml ' + RepositoryPath + ' --non-interactive';
debugln('TSVNStatus.Create CommandLine ' + AProcess.CommandLine);
AProcess.Options := AProcess.Options + [poUsePipes, poStdErrToOutput];
AProcess.ShowWindow := swoHIDE;
AProcess.Execute;
while AProcess.Running do
begin
// make sure we have room
M.SetSize(BytesRead + READ_BYTES);
// try reading it
n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
if n > 0 then
Inc(BytesRead, n)
else
// no data, wait 100 ms
Sleep(100);
end;
// read last part
repeat
// make sure we have room
M.SetSize(BytesRead + READ_BYTES);
// try reading it
n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
if n > 0 then
Inc(BytesRead, n);
until n <= 0;
M.SetSize(BytesRead);
ReadXMLFile(Doc, M);
AProcess.Free;
M.Free;
//now process the XML file
Node := Doc.DocumentElement.FirstChild.FirstChild;
repeat
SubNode := Node;
New(ListItem);
Path := SubNode.Attributes.Item[0].NodeValue;
F:=FileGetAttr(Path);
If F<>-1 then
If (F and faDirectory)=0 then
begin
//initialize author (anonymous repositories)
ListItem^.Author := rsNoAuthor;
//path
ListItem^.Path := Path;
//Extension
ListItem^.Extension:=ExtractFileExt(Path);
//get the wc-status attributes
ListItem^.ItemStatus:='';
ListItem^.Checked:=False;
ListItem^.PropStatus:='';
for i := 0 to SubNode.ChildNodes.Item[0].Attributes.Length -1 do
begin
NodeName := SubNode.ChildNodes.Item[0].Attributes.Item[i].NodeName;
NodeValue := SubNode.ChildNodes.Item[0].Attributes.Item[i].NodeValue;
if NodeName = 'item' then
begin
//ItemStatus
ListItem^.ItemStatus := NodeValue;
//Checked
ListItem^.Checked:=(NodeValue<>'unversioned') and (NodeValue<>'normal');
end;
if NodeName = 'props' then
//PropStatus
ListItem^.PropStatus := NodeValue;
if NodeName = 'revision' then
//Revision
ListItem^.Revision := StrToInt(NodeValue);
end;
//get the commit attributes
SubNode := SubNode.ChildNodes.Item[0].ChildNodes.Item[0];
if Assigned(SubNode) then
begin
//CommitRevision
ListItem^.CommitRevision:=StrToInt(SubNode.Attributes.Item[0].NodeValue);
for i := 0 to SubNode.ChildNodes.Length - 1 do
begin
ActNode := SubNode.ChildNodes.Item[i];
if Assigned(ActNode) then
begin
NodeName := ActNode.NodeName;
//Author
if NodeName = 'author' then
ListItem^.Author := ActNode.FirstChild.NodeValue;
//Date
if NodeName = 'date' then
ListItem^.Date := ISO8601ToDateTime(ActNode.FirstChild.NodeValue);
end;
end;
end;
List.Add(ListItem);
end;
Node := Node.NextSibling;
until not Assigned(Node);
end;
destructor TSVNStatus.Destroy;
begin
List.Free;
inherited Destroy;
end;
procedure TSVNStatus.Sort(ASortItem: TStatusItemName; ADirection: TSortDirection);
begin
SortDirection := ADirection;
SortItem := ASortItem;
if ADirection = sdDescending then
case ASortItem of
siChecked: List.Sort(@SortSelectedAscending);
siPath: List.Sort(@SortPathAscending);
siExtension: List.Sort(@SortExtensionAscending);
siItemStatus: List.Sort(@SortItemStatusAscending);
siPropStatus: List.Sort(@SortPropStatusAscending);
siAuthor: List.Sort(@SortPropertyAuthorAscending);
siRevision: List.Sort(@SortPropertyRevisionAscending);
siCommitRevision: List.Sort(@SortPropertyCommitRevisionAscending);
siDate: List.Sort(@SortPropertyDateAscending);
end
else
case ASortItem of
siChecked: List.Sort(@SortSelectedDescending);
siPath: List.Sort(@SortPathDescending);
siExtension: List.Sort(@SortExtensionDescending);
siItemStatus: List.Sort(@SortItemStatusDescending);
siPropStatus: List.Sort(@SortPropStatusDescending);
siAuthor: List.Sort(@SortPropertyAuthorDescending);
siRevision: List.Sort(@SortPropertyRevisionDescending);
siCommitRevision: List.Sort(@SortPropertyCommitRevisionDescending);
siDate: List.Sort(@SortPropertyDateDescending);
end;
end;
procedure TSVNStatus.ReverseSort(ASortItem: TStatusItemName);
begin
if SortItem = ASortItem then
begin
if SortDirection = sdDescending then
Sort(ASortItem, sdAscending)
else
Sort(ASortItem, sdDescending)
end
else
Sort(ASortItem, sdAscending);
end;
initialization
//GetAppConfigDir(False) + 'lazsvnsettings.xml';
SVNSettings := TSVNSettings.Create('lazsvnsettings.xml');
finalization
SVNSettings.Free;
end.