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