1 { Copyright (C) 2008 Darius Blaszijk
2 
3   This source is free software; you can redistribute it and/or modify it under
4   the terms of the GNU General Public License as published by the Free
5   Software Foundation; either version 2 of the License, or (at your option)
6   any later version.
7 
8   This code is distributed in the hope that it will be useful, but WITHOUT ANY
9   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10   FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
11   details.
12 
13   A copy of the GNU General Public License is available on the World Wide Web
14   at <http://www.gnu.org/copyleft/gpl.html>. You can also obtain it by writing
15   to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
16   Boston, MA 02110-1335, USA.
17 }
18 
19 unit SVNClasses;
20 
21 {$mode objfpc}{$H+}
22 
23 interface
24 
25 uses
26   Classes, SysUtils, ComCtrls, FileUtil, UTF8Process, LCLProc, Controls,
27   XMLRead, DOM, Process, StdCtrls, Forms, fgl;
28 
29 resourcestring
30   rsAction = 'Action';
31   rsAdded = 'Added';
32   rsAdd = 'Add to version control';
33   rsAuthor = 'Author';
34   rsCommit = 'Commit';
35   rsCommitRevision = 'Commit revision';
36   rsConflict = 'Conflict';
37   rsCopyFromPath = 'Copy from path';
38   rsCreatePatchFile = 'Create patch file';
39   rsDate = 'Date';
40   rsDelete = 'Delete';
41   rsDeleted = 'Deleted';
42   rsDiffActiveFile = 'Diff active file';
43   rsEdit = 'Edit';
44   rsExtension = 'Extension';
45   rsFileNotInWorkingCopyAnymore = 'File is not part of local working copy (anymore)';
46   rsFileStatus = 'File status';
47   rsIndexOutOfBoundsD = 'Index out of bounds (%d)';
48   rsLazarusSVNCommit = 'LazarusSVN Commit';
49   rsLazarusSVNDiff = '%s - LazarusSVN Diff ...';
50   rsLazarusSVNLog = '%s - LazarusSVN Log ...';
51   rsLazarusSVNUpdate = '%s - LazarusSVN Update ...';
52   rsCommitMsgHistory = 'Commit Message History';
53   rsCommitMsg = 'Commit Message';
54   rsMerged = 'Merged';
55   rsMessage = 'Message';
56   rsNoAuthor = '(no author)';
57   rsOpenFileInEditor = 'Open file in editor';
58   rsOpenThisRevisionInEditor = 'Open this revision in editor';
59   rsOpenPreviousRevisionInEditor = 'Open previous revision in editor';
60   rsOnlyModifiedItemsCanBeDiffed = 'Only modified (M) Items can be diffed';
61   rsPath = 'Path';
62   rsProjectFilename = 'Project filename';
63   rsProjectIsActive = 'Project is active';
64   rsProjectIsNotActiveInSVNSettingsPleaseActivateFirst = 'Project is not '
65     +'active in SVN settings, please activate first.';
66   rsProjectName = 'Project name';
67   rsProjectOptions = 'Project options';
68   rsPropertyStatus = 'Property status';
69   rsRemove = 'Remove from version control (keep local)';
70   rsRepositoryPath = 'Repository path';
71   rsRevert = 'Revert';
72   rsRevision = 'Revision';
73   rsSave = 'Save';
74   rsSettings = 'Settings';
75   rsSVNSettings = 'SVN settings';
76   rsShowDiff = 'Show diff';
77   rsShowDiffBase = 'Show Diff of Local Changes';
78   rsShowDiffPrev = 'Show Diff Against Previous Version';
79   rsShowDiffHead = 'Show Diff Against HEAD';
80   rsShowDiffCountRev = 'Show Last X Commits';
81   rsRefresh = 'Refresh';
82   rsShowLog = 'Show log';
83   rsSourceFileDoesNotBelongToTheProjectPleaseAddFirst = 'Source file does not '
84     +'belong to the project. Please add first.';
85   rsSVNTools = 'SVN tools';
86   rsUpdate = 'Update';
87   rsUpdated = 'Updated';
88 
89 const
90    READ_BYTES = 2048;
91    SVN_REPOSITORY = 'SVN repository';
92    SVN_ACTIVE = 'SVN active';
93 
94 type
95   TSortDirection  = (sdAscending, sdDescending);
96 
97   TStatusItemName = (siChecked, siPath, siExtension, siPropStatus, siItemStatus,
98                      siRevision, siCommitRevision, siAuthor, siDate);
99 
100 //  PSVNStatusItem = ^TSVNStatusItem;
101   TSVNStatusItem = class //record
102     Checked: boolean;
103     Path: string;
104     Extension: string;
105     PropStatus: string;
106     ItemStatus: string;
107     Revision: integer;
108     CommitRevision: integer;
109     Author: string;
110     Date: TDate;
111   end;
112 
113   TSVNStatusList = specialize TFPGObjectList<TSVNStatusItem>;
114 
115   { TSVNStatus }
116 
117   TSVNStatus = class(TObject)
118   private
119     FRepositoryPath: string;
120     FSortDirection: TSortDirection;
121     FSortItem: TStatusItemName;
122   public
123     List: TSVNStatusList; // TFPList;
124 
125     constructor Create(const ARepoPath: string; verbose: Boolean);
126     destructor Destroy; override;
127 
128     property RepositoryPath: string read FRepositoryPath write FrepositoryPath;
129     procedure Sort(ASortItem: TStatusItemName; ADirection: TSortDirection);
130     procedure ReverseSort(ASortItem: TStatusItemName);
131     property SortDirection: TSortDirection read FSortDirection write FSortDirection;
132     property SortItem: TStatusItemName read FSortItem write FSortItem;
133   end;
134 
135 procedure CmdLineToMemo(CmdLine: string; Memo: TMemo);
ExecuteSvnReturnXmlnull136 function ExecuteSvnReturnXml(ACommand: string): TXMLDocument;
137 procedure SetColumn(ListView: TListView; ColNo, DefaultWidth: integer; AName: string; AutoSize: boolean = true);
SVNExecutablenull138 function SVNExecutable: string;
ReplaceLineEndingsnull139 function ReplaceLineEndings(const s, NewLineEnds: string): string;
ISO8601ToDateTimenull140 function ISO8601ToDateTime(ADateTime: string): TDateTime;
141 
142 implementation
143 
144 
145 
146 procedure CmdLineToMemo(CmdLine: string; Memo: TMemo);
147 var
148   AProcess: TProcessUTF8;
149   BytesRead: LongInt;
150   n: LongInt;
151   M: TMemoryStream;
152 
153   procedure UpdateMemoFromStream;
154   var
155     s: string;
156   begin
157     if BytesRead > 0 then begin
158       SetLength(s, BytesRead);
159       M.Read(s[1], BytesRead);
160 
161       // this woks exactly like Append() only without the newline bug
162       Memo.SelText := ReplaceLineEndings(s, LineEnding);
163 
164       M.SetSize(0);
165       BytesRead:=0;
166     end;
167   end;
168 
169 begin
170   AProcess := TProcessUTF8.Create(nil);
171   AProcess.CommandLine := CmdLine;
172   debugln('CmdLineToMemo commandline=', AProcess.CommandLine);
173   AProcess.Options := AProcess.Options + [poUsePipes, poStdErrToOutput];
174   AProcess.ShowWindow := swoHIDE;
175   AProcess.Execute;
176 
177   M := TMemoryStream.Create;
178   BytesRead := 0;
179   Memo.Lines.Text := '';
180 
181   while AProcess.Running do
182   begin
183     // make sure we have room
184     M.SetSize(BytesRead + READ_BYTES);
185 
186     // try reading it
187     n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
188     if n > 0
189     then begin
190       Inc(BytesRead, n);
191       UpdateMemoFromStream;
192       Application.ProcessMessages;
193     end
194     else
195       // no data, wait 100 ms
196       Sleep(100);
197   end;
198   // read last part
199   repeat
200     // make sure we have room
201     M.SetSize(BytesRead + READ_BYTES);
202     // try reading it
203     n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
204     if n > 0
205     then begin
206       Inc(BytesRead, n);
207       UpdateMemoFromStream;
208       Application.ProcessMessages;
209     end;
210   until n <= 0;
211 
212   AProcess.Free;
213   M.Free;
214 
215   Memo.Cursor:=crDefault;
216 end;
217 
218 procedure SetColumn(ListView: TListView; ColNo, DefaultWidth: integer; AName: string; AutoSize: boolean = true);
219 begin
220   ListView.Column[ColNo].Caption:=AName;
221   ListView.Column[ColNo].AutoSize:=AutoSize;
222   ListView.Column[ColNo].Width:=DefaultWidth;
223 end;
224 
SVNExecutablenull225 function SVNExecutable: string;
226 begin
227   //encapsulate with " because of the incompatibility on windows
228   //when svn in in "Program Files" directory
229   Result := '"' + FindDefaultExecutablePath('svn') + '"';
230 end;
231 
ReplaceLineEndingsnull232 function ReplaceLineEndings(const s, NewLineEnds: string): string;
233 var
234   p: Integer;
235   StartPos: LongInt;
236 begin
237   Result:=s;
238   p:=1;
239   while (p<=length(Result)) do begin
240     if Result[p] in [#10,#13] then begin
241       StartPos:=p;
242       if (p<length(Result))
243       and (Result[p+1] in [#10,#13]) and (Result[p]<>Result[p+1]) then
244         inc(p);
245       Result:=copy(Result,1,StartPos-1)+NewLineEnds+copy(Result,p+1,length(Result));
246       inc(p,length(NewLineEnds));
247     end else begin
248       inc(p);
249     end;
250   end;
251 end;
252 
ISO8601ToDateTimenull253 function ISO8601ToDateTime(ADateTime: string): TDateTime;
254 var
255   y, m, d, h, n, s: word;
256 begin
257   y := StrToInt(Copy(ADateTime, 1, 4));
258   m := StrToInt(Copy(ADateTime, 6, 2));
259   d := StrToInt(Copy(ADateTime, 9, 2));
260   h := StrToInt(Copy(ADateTime, 12, 2));
261   n := StrToInt(Copy(ADateTime, 15, 2));
262   s := StrToInt(Copy(ADateTime, 18, 2));
263 
264   Result := ComposeDateTime( EncodeDate(y,m,d), EncodeTime(h,n,s,0));
265 end;
266 
SortPathAscendingnull267 function SortPathAscending(const Item1, Item2: TSVNStatusItem): Integer;
268 begin
269    Result := CompareText(Item1.Path, Item2.Path);
270 end;
271 
SortPathDescendingnull272 function SortPathDescending(const Item1, Item2: TSVNStatusItem): Integer;
273 begin
274   Result := -SortPathAscending(Item1, Item2);
275 end;
276 
SortSelectedAscendingnull277 function SortSelectedAscending(const Item1, Item2: TSVNStatusItem): Integer;
278 begin
279    if Item1.Checked > Item2.Checked then
280      Result := 1
281    else
282      if Item1.Checked = Item2.Checked then
283        Result := SortPathDescending(Item1, Item2)
284      else
285        Result := -1;
286 end;
287 
SortSelectedDescendingnull288 function SortSelectedDescending(const Item1, Item2: TSVNStatusItem): Integer;
289 begin
290   Result := -SortSelectedAscending(Item1, Item2);
291 end;
292 
SortExtensionAscendingnull293 function SortExtensionAscending(const Item1, Item2: TSVNStatusItem): Integer;
294 begin
295    Result := CompareText(Item1.Extension, Item2.Extension);
296 end;
297 
SortExtensionDescendingnull298 function SortExtensionDescending(const Item1, Item2: TSVNStatusItem): Integer;
299 begin
300   Result := -SortExtensionAscending(Item1, Item2);
301 end;
302 
SortItemStatusAscendingnull303 function SortItemStatusAscending(const Item1, Item2: TSVNStatusItem): Integer;
304 begin
305    Result := CompareText(Item1.ItemStatus, Item2.ItemStatus);
306 end;
307 
SortItemStatusDescendingnull308 function SortItemStatusDescending(const Item1, Item2: TSVNStatusItem): Integer;
309 begin
310   Result := -SortItemStatusAscending(Item1, Item2);
311 end;
312 
SortPropStatusAscendingnull313 function SortPropStatusAscending(const Item1, Item2: TSVNStatusItem): Integer;
314 begin
315    Result := CompareText(Item1.PropStatus, Item2.PropStatus);
316 end;
317 
SortPropStatusDescendingnull318 function SortPropStatusDescending(const Item1, Item2: TSVNStatusItem): Integer;
319 begin
320   Result := -SortPropStatusAscending(Item1, Item2);
321 end;
322 
SortPropertyAuthorAscendingnull323 function SortPropertyAuthorAscending(const Item1, Item2: TSVNStatusItem): Integer;
324 begin
325    Result := CompareText(Item1.Author, Item2.Author);
326 end;
327 
SortPropertyAuthorDescendingnull328 function SortPropertyAuthorDescending(const Item1, Item2: TSVNStatusItem): Integer;
329 begin
330   Result := -SortPropertyAuthorAscending(Item1, Item2);
331 end;
332 
SortPropertyRevisionAscendingnull333 function SortPropertyRevisionAscending(const Item1, Item2: TSVNStatusItem): Integer;
334 begin
335    if Item1.Revision > Item2.Revision then
336      Result := 1
337    else
338      if Item1.Revision = Item2.Revision then
339        Result := 0
340      else
341        Result := -1;
342 end;
343 
SortPropertyRevisionDescendingnull344 function SortPropertyRevisionDescending(const Item1, Item2: TSVNStatusItem): Integer;
345 begin
346   Result := -SortPropertyRevisionAscending(Item1, Item2);
347 end;
348 
SortPropertyCommitRevisionAscendingnull349 function SortPropertyCommitRevisionAscending(const Item1, Item2: TSVNStatusItem): Integer;
350 begin
351    if Item1.CommitRevision > Item2.CommitRevision then
352      Result := 1
353    else
354      if Item1.CommitRevision = Item2.CommitRevision then
355        Result := 0
356      else
357        Result := -1;
358 end;
359 
SortPropertyCommitRevisionDescendingnull360 function SortPropertyCommitRevisionDescending(const Item1, Item2: TSVNStatusItem): Integer;
361 begin
362   Result := -SortPropertyCommitRevisionAscending(Item1, Item2);
363 end;
364 
SortPropertyDateAscendingnull365 function SortPropertyDateAscending(const Item1, Item2: TSVNStatusItem): Integer;
366 begin
367    if Item1.Date > Item2.Date then
368      Result := 1
369    else
370      if Item1.Date = Item2.Date then
371        Result := 0
372      else
373        Result := -1;
374 end;
375 
SortPropertyDateDescendingnull376 function SortPropertyDateDescending(const Item1, Item2: TSVNStatusItem): Integer;
377 begin
378   Result := -SortPropertyDateAscending(Item1, Item2);
379 end;
380 
ExecuteSvnReturnXmlnull381 function ExecuteSvnReturnXml(ACommand: string): TXMLDocument;
382 var
383   AProcess: TProcessUTF8;
384   M: TMemoryStream;
385   n, BytesRead: Integer;
386 begin
387   AProcess := TProcessUTF8.Create(nil);
388   AProcess.CommandLine := SVNExecutable + ' ' + ACommand;
389   debugln('TSVNLogFrm.ExecuteSvnReturnXml CommandLine ' + AProcess.CommandLine);
390   AProcess.Options := AProcess.Options + [poUsePipes, poStdErrToOutput];
391   AProcess.ShowWindow := swoHIDE;
392   AProcess.Execute;
393 
394   M := TMemoryStream.Create;
395   BytesRead := 0;
396 
397   while AProcess.Running do
398   begin
399     // make sure we have room
400     M.SetSize(BytesRead + READ_BYTES);
401 
402     // try reading it
403     n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
404     if n > 0
405     then begin
406       Inc(BytesRead, n);
407     end
408     else begin
409       // no data, wait 100 ms
410       Sleep(100);
411     end;
412   end;
413 
414   // read last part
415   repeat
416     // make sure we have room
417     M.SetSize(BytesRead + READ_BYTES);
418 
419     // try reading it
420     n := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
421     if n > 0
422     then begin
423       Inc(BytesRead, n);
424     end;
425   until n <= 0;
426   M.SetSize(BytesRead);
427 
428   ReadXMLFile(Result, M);
429 
430   M.Free;
431   AProcess.Free;
432 end;
433 
434 { TSVNStatus }
435 
436 constructor TSVNStatus.Create(const ARepoPath: string; Verbose: Boolean);
437 var
438   ActNode: TDOMNode;
439   Doc: TXMLDocument;
440   F: LongInt;
441   i: integer;
442   ListItem: TSVNStatusItem;
443   Node: TDOMNode;
444   NodeName: string;
445   NodeValue: string;
446   Path: string;
447   SubNode: TDOMNode;
448 begin
449   List := TSVNStatusList.Create;
450   RepositoryPath := ARepoPath;
451 
452   if Verbose then
453     Doc := ExecuteSvnReturnXml('stat --verbose --xml "' + RepositoryPath  + '" --non-interactive')
454   else
455     Doc := ExecuteSvnReturnXml('stat --xml "' + RepositoryPath  + '" --non-interactive');
456 
457   Node := Doc.DocumentElement.FirstChild.FirstChild;
458   if Node = nil then begin
459     // no <entry> node found, list is empty.
460     Doc.Free;
461     Exit();
462   end;
463 
464   repeat
465     SubNode := Node;
466     Path := SubNode.Attributes.Item[0].NodeValue;
467     debugln('TSVNStatus.Create ' + Path);
468     F:=FileGetAttr(Path);
469     If (F<>-1) and ((F and faDirectory)=0) then
470     begin
471       ListItem := TSVNStatusItem.Create;
472       //initialize author (anonymous repositories)
473       ListItem.Author := rsNoAuthor;
474       //path
475       ListItem.Path := Path;
476       //Extension
477       ListItem.Extension:=ExtractFileExt(Path);
478       //get the wc-status attributes
479       ListItem.ItemStatus:='';
480       ListItem.Checked:=False;
481       ListItem.PropStatus:='';
482       for i := 0 to SubNode.ChildNodes.Item[0].Attributes.Length -1 do
483       begin
484         NodeName := SubNode.ChildNodes.Item[0].Attributes.Item[i].NodeName;
485         NodeValue := SubNode.ChildNodes.Item[0].Attributes.Item[i].NodeValue;
486         if NodeName = 'item' then
487         begin
488           //ItemStatus
489           ListItem.ItemStatus := LowerCase(NodeValue);
490           //Checked
491           ListItem.Checked:=(NodeValue<>'unversioned') and (NodeValue<>'normal');
492         end;
493         if NodeName = 'props' then
494           //PropStatus
495           ListItem.PropStatus := NodeValue;
496         if NodeName = 'revision' then
497           //Revision
498           ListItem.Revision := StrToInt(NodeValue);
499       end;
500       //get the commit attributes
501       SubNode := SubNode.ChildNodes.Item[0].ChildNodes.Item[0];
502       if Assigned(SubNode) then
503       begin
504         //CommitRevision
505         ListItem.CommitRevision:=StrToInt(SubNode.Attributes.Item[0].NodeValue);
506         for i := 0 to SubNode.ChildNodes.Count - 1 do
507         begin
508           ActNode := SubNode.ChildNodes.Item[i];
509           if Assigned(ActNode) then
510           begin
511             NodeName := ActNode.NodeName;
512             //Author
513             if NodeName = 'author' then
514               ListItem.Author := ActNode.FirstChild.NodeValue;
515             //Date
516             if NodeName = 'date' then
517               ListItem.Date := ISO8601ToDateTime(ActNode.FirstChild.NodeValue);
518           end;
519         end;
520       end;
521       List.Add(ListItem);
522     end;
523     Node := Node.NextSibling;
524   until not Assigned(Node);
525   Doc.Free;
526 end;
527 
528 destructor TSVNStatus.Destroy;
529 begin
530   List.Free;
531   inherited Destroy;
532 end;
533 
534 procedure TSVNStatus.Sort(ASortItem: TStatusItemName; ADirection: TSortDirection);
535 begin
536   SortDirection := ADirection;
537   SortItem := ASortItem;
538 
539   if ADirection = sdDescending then
540     case ASortItem of
541       siChecked:        List.Sort(@SortSelectedAscending);
542       siPath:           List.Sort(@SortPathAscending);
543       siExtension:      List.Sort(@SortExtensionAscending);
544       siItemStatus:     List.Sort(@SortItemStatusAscending);
545       siPropStatus:     List.Sort(@SortPropStatusAscending);
546       siAuthor:         List.Sort(@SortPropertyAuthorAscending);
547       siRevision:       List.Sort(@SortPropertyRevisionAscending);
548       siCommitRevision: List.Sort(@SortPropertyCommitRevisionAscending);
549       siDate:           List.Sort(@SortPropertyDateAscending);
550     end
551   else
552     case ASortItem of
553       siChecked:        List.Sort(@SortSelectedDescending);
554       siPath:           List.Sort(@SortPathDescending);
555       siExtension:      List.Sort(@SortExtensionDescending);
556       siItemStatus:     List.Sort(@SortItemStatusDescending);
557       siPropStatus:     List.Sort(@SortPropStatusDescending);
558       siAuthor:         List.Sort(@SortPropertyAuthorDescending);
559       siRevision:       List.Sort(@SortPropertyRevisionDescending);
560       siCommitRevision: List.Sort(@SortPropertyCommitRevisionDescending);
561       siDate:           List.Sort(@SortPropertyDateDescending);
562     end;
563 end;
564 
565 procedure TSVNStatus.ReverseSort(ASortItem: TStatusItemName);
566 begin
567   if SortItem = ASortItem then
568   begin
569      if SortDirection = sdDescending then
570        Sort(ASortItem, sdAscending)
571      else
572        Sort(ASortItem, sdDescending)
573   end
574   else
575     Sort(ASortItem, sdAscending);
576 end;
577 
578 end.
579 
580