MODULE SVNAdmin; (** AUTHOR "rstoll"; *)

(*
	.svn directories are per default write protected. Changing file properties is only supported in the Win32 version
	by kernel calls.
*)

IMPORT
	SVNOutput, SVNUtil,
	Strings, Dates,
	Commands, Files, Kernel32, KernelLog;

CONST
	EntryFileFormat* = 8; (* expected svn version *)

TYPE

	EntryEntity* = RECORD
		Format* : LONGINT; (* 1 *)
		Name* : ARRAY 256 OF CHAR; (* 2 *)
		NodeKind* : ARRAY 32 OF CHAR; (* 3 *)
		Revision* : LONGINT; (* 4 *)
		GlobalRemoval* : BOOLEAN;
		Url*, UrlConst* : ARRAY 256 OF CHAR; (* 5 *)
		RepositoryRoot* : ARRAY 256 OF CHAR; (* 6 *)
		Schedule* : ARRAY 32 OF CHAR; (* 7 *)
		TextLastUpdated* : ARRAY 32 OF CHAR; (* 8 *)
		Checksum* : ARRAY 33 OF CHAR; (* 9 *)
		LastChangedDate* : ARRAY 32 OF CHAR; (* 10: len ?= 28? *)
		LastChangedRevision* : LONGINT; (* 11 *)
		LastChangedAuthor* : ARRAY 256 OF CHAR; (* 12 *)
		Props* : ARRAY 256 OF CHAR; (* 13: maybe longer? *)
		(*PropsMods : ARRAY 256 OF CHAR; (* 14: maybe longer *)*)
		RepositoryUUID* : ARRAY 37 OF CHAR; (* 27 *)

		VersionUrl* : ARRAY 256 OF CHAR;
	END;

TYPE
	TraverseHandler* = PROCEDURE {DELEGATE} ( CONST path : ARRAY OF CHAR; fileEntry : EntryEntity; data : ANY ) : BOOLEAN;

TYPE

	Entry* = OBJECT
	VAR
		adminDir : EntryEntity;
		path : ARRAY 256 OF CHAR;
		name : ARRAY 32 OF CHAR;
		context : Commands.Context;
		fAdmin, fAdminTemp : Files.File;
		entriesfile, entriesfiletemp, svnpath : ARRAY 256 OF CHAR;
		pathIsFile, readGlobalData, readFromTempfile : BOOLEAN;
		r : Files.Reader;
		w : Files.Writer;
		pathLength : LONGINT;

		PROCEDURE &Init* ( c: Commands.Context );
		BEGIN
			context := c;
			readGlobalData := FALSE;
		END Init;

		PROCEDURE ReadVersionUrl* ( CONST filename : ARRAY OF CHAR );
		VAR
			propsfile, p, n, tmp : ARRAY 256 OF CHAR;
			nextFileEntry : BOOLEAN;
			pf : Files.File;
			pr : Files.Reader;
		BEGIN
			IF fAdmin # NIL THEN

				IF pathIsFile THEN
					Files.SplitPath ( path, p, n );
					Files.JoinPath ( p, ".svn/all-wcprops", propsfile );
				ELSE
					Files.JoinPath ( path, ".svn/all-wcprops", propsfile );
				END;

				IF filename # "" THEN
					COPY ( filename, n );
				END;

				pf := Files.Old ( propsfile );
				ASSERT ( pf # NIL );

				Files.OpenReader ( pr, pf, 0 );
				ASSERT ( pr # NIL );

				IF pathIsFile OR (n # "") THEN (* search for the file *)
					nextFileEntry := FALSE;
					LOOP
						pr.Ln ( tmp );
						IF pr.res # Files.Ok THEN
							KernelLog.String ( "ERROR: didn't find entry in the .svn/all-wcprops file for: " );
							KernelLog.String ( p ); KernelLog.String ( " file: " ); KernelLog.String ( n );
							KernelLog.Ln;
							RETURN;
						END;

						IF nextFileEntry & (tmp = n) THEN EXIT END;
						nextFileEntry := (tmp = "END");
					END;
				END;

				(* get url *)
				pr.SkipLn; pr.SkipLn; pr.SkipLn;
				pr.Ln ( adminDir.VersionUrl );
			END;
		END ReadVersionUrl;

		PROCEDURE SetPath* ( CONST p : ARRAY OF CHAR; VAR res : LONGINT );
		BEGIN
			r := NIL;
			readGlobalData := FALSE;
			COPY ( p, path );

			AnalyzePath ( res );

			ASSERT ( (res = SVNOutput.ResOK) OR (res = SVNOutput.ResNOTVERSIONED) );
		END SetPath;

		PROCEDURE AnalyzePath ( VAR res : LONGINT );
		BEGIN
			pathIsFile := FALSE;
			res := SVNOutput.ResOK;

			IF (path = ".") OR (path = "") THEN
				fAdmin := Files.Old (".svn/entries");

				IF fAdmin = NIL THEN
					res := SVNOutput.ResNOTVERSIONED;
					RETURN;
				END;
			ELSE
				Files.JoinPath ( path, ".svn/entries", entriesfile );
				fAdmin := Files.Old ( entriesfile );

				IF fAdmin = NIL THEN (* our parameter was a file destination *)
					Files.SplitPath ( path, svnpath, name );
					Files.JoinPath ( svnpath, ".svn/entries", entriesfile );
					fAdmin := Files.Old ( entriesfile );

					IF fAdmin = NIL THEN
						res := SVNOutput.ResNOTVERSIONED;
						RETURN;
					END;

					pathIsFile := TRUE; (* search the file in the svn-entries file *)

					Files.JoinPath ( svnpath, ".svn/tmp/od-entries", entriesfiletemp );
				ELSE
(*					Kernel32.SetFileAttributes ( entriesfile, {} );*)
					Files.JoinPath ( path, ".svn/tmp/od-entries", entriesfiletemp );
				END;
			END;

			ASSERT ( fAdmin # NIL );
		END AnalyzePath;

		PROCEDURE CreateTempfile*;
		VAR
			res : LONGINT;
		BEGIN
			IF SVNUtil.FileExists ( entriesfiletemp ) THEN
				Files.Delete ( entriesfiletemp, res );
				ASSERT ( res = Files.Ok );
			END;
			fAdminTemp := Files.New ( entriesfiletemp );

			ReadFromTempfile ( TRUE );
		END CreateTempfile;

		PROCEDURE ReadFromTempfile* ( b : BOOLEAN );
		BEGIN
			IF b THEN
				ASSERT ( fAdmin # NIL );
				ASSERT ( fAdminTemp # NIL );

				Files.OpenWriter ( w, fAdminTemp, 0 );
				Files.OpenReader ( r, fAdmin, 0 );
			END;
			readFromTempfile := b;
		END ReadFromTempfile;

		PROCEDURE GetUrl* ( VAR url : ARRAY OF CHAR );
		BEGIN
			COPY ( adminDir.Url, url );
		END GetUrl;

		PROCEDURE GetRepo* ( VAR repos : ARRAY OF CHAR );
		BEGIN
			COPY ( adminDir.RepositoryRoot, repos );
		END GetRepo;

		PROCEDURE GetVersion* () : LONGINT;
		BEGIN
			IF ~pathIsFile THEN
				RETURN adminDir.Revision;
			ELSE
				RETURN adminDir.LastChangedRevision;
			END;
		END GetVersion;

		PROCEDURE SkipGlobalData;
		VAR
			temp : ARRAY 256 OF CHAR;
			start: ARRAY 2 OF CHAR;
		BEGIN
(*			IF ~readGlobalData THEN
				readGlobalData := TRUE;*)

				ASSERT ( fAdmin # NIL );
				NEW( r, fAdmin, 0 );
				start[0] := CHR(12); start[1] := 0X;
				REPEAT
					r.Ln ( temp );
					IF readFromTempfile THEN
						WriteString ( w, temp );
					END;
				UNTIL Strings.StartsWith2(start, temp);
(*			END;*)
		END SkipGlobalData;

		(* read all lines until we reach end of entry *)
		PROCEDURE SkipReaderToEOE;
		VAR
			temp : ARRAY 256 OF CHAR;
			start: ARRAY 2 OF CHAR;
		BEGIN
			ASSERT ( r # NIL );
			start[0] := CHR(12); start[1] := 0X;
			REPEAT
				r.Ln ( temp );
				IF r.res # Files.Ok THEN RETURN END;
			UNTIL Strings.StartsWith2(start, temp);
		END SkipReaderToEOE;

		PROCEDURE IsItemVersioned* ( CONST filename : ARRAY OF CHAR ) : BOOLEAN;
		VAR
			temp : ARRAY 256 OF CHAR;
		BEGIN
			(* global data hasn't been read, so we need to do it now *)
			SkipGlobalData;

			readGlobalData := FALSE;

			ASSERT ( r # NIL );

			LOOP
				r.Ln(temp); (* 2: read name of file *)
				IF r.res # Files.Ok THEN RETURN FALSE END;

				IF readFromTempfile THEN
					WriteString ( w, temp );
				END;

				Strings.TrimWS(temp);

				IF filename = temp THEN
					RETURN TRUE;
				ELSE
					IF readFromTempfile THEN
						ReadWriteToEOE;
					ELSE
						SkipReaderToEOE;
					END;
					IF r.res # Files.Ok THEN RETURN FALSE END;
				END;
			END;
		END IsItemVersioned;

		(* add everything in the directory recursively
			- path
			- name: name of the directory inside the path which will be added
			- addGlobal: usually the first
		*)
		PROCEDURE Add* ( CONST path : ARRAY OF CHAR; CONST name : ARRAY OF CHAR; addGlobal : BOOLEAN; VAR res : LONGINT );
		VAR
			file, entryfile, tmp, tmp2 : Files.FileName;
			res1, time, date, size : LONGINT;
			w : Files.Writer;
			flags : SET;
			enum : Files.Enumerator;
			urlRepo : Strings.String;
		BEGIN
			res := SVNOutput.ResOK;

			SetPath ( path, res1 );
			ASSERT ( res1 = SVNOutput.ResOK );
			ASSERT ( ~pathIsFile );

			IF IsItemVersioned ( name ) THEN
				res := SVNOutput.ResALREADYVERSIONED;
				RETURN;
			END;

			Files.JoinPath ( path, name, file );
			ASSERT ( ~SVNUtil.FileExists ( file ) ); (* 'name' must be a directory *)

			NEW ( enum );
			enum.Open ( file, {} ); (* get all files/dirs in the actual directory *)

			Files.JoinPath ( path, ".svn/entries", entryfile );

			IF addGlobal THEN
				pathLength := Strings.Length(path)+1;

				fAdmin := Files.Old ( entryfile );
				ASSERT ( fAdmin # NIL );
				RemoveFileAttribute2 ( entryfile, fAdmin );

				ReadData ( res1 );

				fAdmin := Files.Old ( entryfile ); (* needed? *)
				Files.OpenWriter ( w, fAdmin, fAdmin.Length() );
				WriteAddEntry ( w, name, FALSE );
				w.Update;

				SetFileAttribute2 ( entryfile, fAdmin );
			END;

			urlRepo := Strings.Substring2 ( pathLength, file );
			w := CreateDummy ( file, urlRepo^ );

			(* add all files/dirs in the actual directory *)
			WHILE enum.HasMoreEntries() DO
				IF enum.GetEntry ( tmp, flags, time, date, size ) THEN
					KernelLog.String ( " A " ); KernelLog.String ( tmp ); KernelLog.Ln;

					Files.SplitPath ( tmp, tmp, tmp2 );

					IF Files.Directory IN flags THEN
						Add ( file, tmp2, FALSE, res );
						ASSERT ( res = SVNOutput.ResOK );
					END;
					(* TODO check whether file is already versioned.... hmm can this even happen?? *)
					WriteAddEntry ( w, tmp2, ~(Files.Directory IN flags) );
				END;
			END;
			w.Update;

			SetFileAttribute2 ( entryfile, fAdmin );
		END Add;

		(* creates a dummy .svn entry; scheduled for adding *)
		PROCEDURE CreateDummy* ( CONST path, urlRepoDir : ARRAY OF CHAR ) : Files.Writer;
		VAR
			entryfile : Files.FileName;
			f : Files.File;
			w : Files.Writer;
			data : EntryEntity;
		BEGIN

			CreateDirectory ( path );

			Files.JoinPath ( path, ".svn/entries", entryfile );
			f := Files.New ( entryfile );
			Files.OpenWriter ( w, f, 0 );

			data.Schedule := "add";
			Files.JoinPath ( adminDir.Url, urlRepoDir, data.Url );
			COPY ( adminDir.RepositoryRoot, data.RepositoryRoot );
			Write ( w, data );

			Files.Register ( f );

			RETURN w;
		END CreateDummy;

		PROCEDURE ReadData* ( VAR res : LONGINT );
		VAR
			i : INTEGER;
			tmp : ARRAY 256 OF CHAR;
			start: ARRAY 2 OF CHAR;
		BEGIN
			res := SVNOutput.ResOK;
			readGlobalData := TRUE; (* now we read global data *)

			Files.OpenReader ( r, fAdmin, 0 );

			r.Int ( adminDir.Format, FALSE ); r.SkipLn; (* 1 *)

			IF adminDir.Format < EntryFileFormat THEN
				res := SVNOutput.ResCLIENTOLD;
				RETURN;
			END;

			r.SkipLn; (* 2: name...first entry is empty *)

			r.Ln ( adminDir.NodeKind ); (* 3 *)
			r.Ln ( tmp ); (* 4 *)
			IF tmp # "" THEN
				Strings.StrToInt ( tmp, adminDir.Revision ); (* 4 *)
			END;
			r.Ln ( adminDir.Url ); (* 5 *)
			COPY ( adminDir.Url, adminDir.UrlConst );
			r.Ln ( adminDir.RepositoryRoot ); (* 6 *)
			r.Ln ( adminDir.Schedule ); (* 7 *)
			adminDir.GlobalRemoval := (adminDir.Schedule = "delete");

			r.SkipLn; (* 8 *)
			r.SkipLn; (* 9 *)

			r.Ln ( adminDir.LastChangedDate ); (* 10 *)
			r.Ln ( tmp ); (* 11 *)
			IF tmp # "" THEN
				Strings.StrToInt ( tmp, adminDir.LastChangedRevision ); (* 11 *)
			END;
			r.Ln ( adminDir.LastChangedAuthor ); (* 12 *)
			r.SkipLn; (* 13 *)

			(* empty line?? *)
			r.SkipLn; (* 14 *)

			r.Ln ( adminDir.Props ); (* 15 *)

			r.Ln ( tmp ); (* 16 *)
			start[0] := CHR(12); start[1] := 0X;
			IF ~Strings.StartsWith2(start, tmp) THEN
				FOR i:=1 TO 10 DO
					r.SkipLn; (* 17-27 *)
				END;

				r.Ln ( adminDir.RepositoryUUID );

				r.SkipLn; (* ^L *)
			END;

			IF pathIsFile THEN (* search for more deatils in entries file... file or directory entry *)
				IF ~IsItemVersioned ( name ) THEN
					res := SVNOutput.ResNOTVERSIONED;
				ELSE
					(* found file entry... read data into adminDir *)
					ReadFileData ( name, res );
				END;
			END;
		END ReadData;

		PROCEDURE ReadFileData ( CONST name : ARRAY OF CHAR; VAR res : LONGINT );
		VAR
			tmp : ARRAY 256 OF CHAR;
			start: ARRAY 2 OF CHAR;
		BEGIN
			res := SVNOutput.ResOK;
			IF name = "" THEN
				r.Ln ( adminDir.Name ); (* 2*)
			ELSE
				COPY ( name, adminDir.Name );
			END;

			Strings.Concat ( adminDir.UrlConst, "/", adminDir.Url ); Strings.Append ( adminDir.Url, adminDir.Name );

			r.Ln ( adminDir.NodeKind ); (* 3*)


			IF r.Peek() # CHR(12) THEN
				r.Ln ( tmp ); (* 4*)
				IF tmp # "" THEN
					Strings.StrToInt ( tmp, adminDir.Revision );
				END;

				r.SkipLn; r.SkipLn; (* 5-6 *)
				r.Ln ( adminDir.Schedule ); (* 7 *)
				IF adminDir.Schedule = "" THEN
					r.Ln ( adminDir.TextLastUpdated ); (* 8 *)
					r.Ln ( adminDir.Checksum ); (* 9 *)
					r.Ln ( adminDir.LastChangedDate ); (* 10 *)
					r.Ln ( tmp ); (* 11 *)
					IF tmp # "" THEN
						Strings.StrToInt ( tmp, adminDir.LastChangedRevision );
					ELSE
						res := SVNOutput.ResNOTVERSIONED;
						RETURN;
					END;
					r.Ln ( adminDir.LastChangedAuthor ); (* 12 *)
				END;
			END;

			(* read all lines until we reach end of entry; including CHR(12) *)
			start[0] := CHR(12); start[1] := 0X;
			REPEAT
				r.Ln ( tmp );
				IF r.res # Files.Ok THEN RETURN END;
			UNTIL Strings.StartsWith2(start, tmp);
		END ReadFileData;

		PROCEDURE PrintData*;
		BEGIN
			context.out.String ("Path: "); context.out.String ( path ); context.out.Ln; context.out.Update;

			context.out.String ("URL: ");
			context.out.String (adminDir.Url);
			context.out.Ln;

			context.out.String ("Repository Root: ");
			context.out.String (adminDir.RepositoryRoot);
			context.out.Ln;

			context.out.String ("Repository UUID: ");
			context.out.String (adminDir.RepositoryUUID);
			context.out.Ln;

			context.out.String ("Revision: ");
			context.out.Int (adminDir.Revision, 0);
			context.out.Ln;

			context.out.String ("Node Kind: ");
			IF adminDir.NodeKind = "dir" THEN
				context.out.String ("directory");
			ELSE
				context.out.String (adminDir.NodeKind);
			END;
			context.out.Ln;

			context.out.String ( "Schedule: " );
			IF (adminDir.Schedule = "") OR (adminDir.Schedule = "normal") THEN
				context.out.String ( "normal" );
			ELSE
				context.out.String ( adminDir.Schedule );
			END;
			context.out.Ln;

			(* if we added a file the rest of the informations aren't important *)
			IF adminDir.Schedule = "add" THEN
				RETURN;
			END;

			context.out.String ("Last Changed Author: ");
			context.out.String (adminDir.LastChangedAuthor);
			context.out.Ln;

			context.out.String ("Last Changed Revision: ");
			context.out.Int (adminDir.LastChangedRevision, 0);
			context.out.Ln;

			context.out.String ("Last Changed Date: ");
			context.out.String (adminDir.LastChangedDate);
			context.out.Ln;

			IF adminDir.NodeKind = "file" THEN
				context.out.String ("Text Last Updated: ");
				context.out.String (adminDir.TextLastUpdated);
				context.out.Ln;

				context.out.String ("Checksum: ");
				context.out.String (adminDir.Checksum);
				context.out.Ln;
			END;

			context.out.Update;

		END PrintData;



		PROCEDURE ReadWriteLines* ( count : LONGINT );
		VAR
			i : LONGINT;
			tmp : ARRAY 256 OF CHAR;
		BEGIN
			FOR i := 1 TO count DO
				IF r.Peek() = CHR(12) THEN
					tmp := "";
				ELSE
					r.Ln ( tmp );

				END;

				ASSERT ( r.res = Files.Ok );
				w.String ( tmp ); w.Char ( 0AX );
			END;
		END ReadWriteLines;

		PROCEDURE ReadWriteLine* ( VAR str : ARRAY OF CHAR );
		BEGIN
			IF r.Peek() # CHR(12) THEN
				r.Ln ( str );
			ELSE
				str := "";
			END;
			w.String ( str );
			w.Char ( 0AX );
		END ReadWriteLine;

		PROCEDURE ReadWriteRest*;
		VAR
			tmp : ARRAY 256 OF CHAR;
			len : LONGINT;
		BEGIN
			REPEAT
				r.Bytes ( tmp, 0, LEN(tmp), len );
				w.Bytes ( tmp, 0, len );
			UNTIL len < LEN(tmp);
		END ReadWriteRest;

		PROCEDURE ReadWriteString* ( CONST str : ARRAY OF CHAR );
		BEGIN
			IF r.Peek() # CHR(12) THEN
				r.SkipLn;
			END;
			w.String ( str );
			w.Char ( 0AX );
		END ReadWriteString;

		(* EOE = end of entry CHR(12) *)
		PROCEDURE ReadWriteToEOE*;
		VAR
			tmp : ARRAY 256 OF CHAR; start: ARRAY 2 OF CHAR;
		BEGIN
			start[0] := CHR(12); start[1] := 0X;
			REPEAT
				r.Ln ( tmp );
				IF r.res # Files.Ok THEN RETURN END;
				w.String ( tmp ); w.Char ( 0AX );
			UNTIL Strings.StartsWith2(start, tmp);
		END ReadWriteToEOE;

		PROCEDURE IsEOF* () : BOOLEAN;
		VAR
			c : CHAR;
		BEGIN
			c := r.Peek();
			RETURN (r.res # Files.Ok) OR (c = 0X);
		END IsEOF;

		PROCEDURE WriteUpdate*;
		VAR
			res : LONGINT;
			overwrite : BOOLEAN;
		BEGIN
			w.Update;
			Files.Register ( fAdminTemp );

			overwrite := TRUE;
			RemoveFileAttribute ( entriesfile );
			Files.CopyFile ( entriesfiletemp, entriesfile, overwrite, res );
			SetFileAttribute ( entriesfile );
			ASSERT ( res = Files.Ok );
		END WriteUpdate;

		PROCEDURE ReadWriteEOE*;
		BEGIN
			SkipReaderToEOE;
			w.Char ( CHR(12) ); w.Char ( 0AX );
		END ReadWriteEOE;


	END Entry;







	PROCEDURE WriteAddEntry* ( w : Files.Writer; CONST name : ARRAY OF CHAR; file : BOOLEAN );
	VAR
		data : EntryEntity;
	BEGIN
		COPY ( name, data.Name );
		data.Schedule := "add";
		IF file THEN data.NodeKind := "file" ELSE data.NodeKind := "dir" END;

		Write ( w, data );
	END WriteAddEntry;

	PROCEDURE WriteString ( w : Files.Writer; CONST line : ARRAY OF CHAR );
	BEGIN
		w.String ( line );
		w.Char ( 0AX );
	END WriteString;

	PROCEDURE WriteInt ( w : Files.Writer; line : LONGINT );
	BEGIN
		IF line # 0 THEN
			w.Int ( line, 0 );
		END;
		w.Char ( 0AX );
	END WriteInt;

	PROCEDURE Write* ( w : Files.Writer; data : EntryEntity );
	VAR
		i : LONGINT;
		tmp : ARRAY 34 OF CHAR;
	BEGIN
		IF data.Name = "" THEN
			(* write the header section in .svn/entries *)
			WriteInt ( w, EntryFileFormat ); (* 1 *)
			w.Char ( 0AX ); (* 2 *)
			WriteString ( w, "dir" ); (* 3 *)
			IF data.Schedule = "add" THEN
				w.String ( "0" ); w.Char ( 0AX ); (* 4 *)
			ELSE
				WriteInt ( w, data.Revision ); (* 4 *)
			END;
			WriteString ( w, data.Url ); (* 5 *)
			WriteString ( w, data.RepositoryRoot ); (* 6 *)
			WriteString ( w, data.Schedule ); (* 7 *)
			w.Char ( 0AX ); (* 8 *)
			w.Char ( 0AX ); (* 9 *)
			WriteString ( w, data.LastChangedDate ); (* 10 *)
			WriteInt ( w, data.LastChangedRevision ); (* 11 *)
			WriteString ( w, data.LastChangedAuthor ); (* 12 *)
			WriteString ( w, data.Props ); (* 13 *)
			w.Char ( 0AX ); (* 14 *)
			w.String ( "svn:special svn:externals svn:needs-lock" ); w.Char ( 0AX ); (* 15 *)

			IF data.Schedule = "" THEN
				FOR i := 16 TO 26 DO w.Char ( 0AX ) END;
				WriteString ( w, data.RepositoryUUID ); (* 27 *)
			END;
		ELSIF data.NodeKind = "file" THEN
			(* write the file section in .svn/entries *)
			WriteString ( w, data.Name ); (* 2 *)
			w.String ( "file" ); w.Char ( 0AX ); (* 3 *)
			IF data.Schedule = "add" THEN
				w.String ( "0" ); w.Char ( 0AX ); (* 4 *)
			ELSE
				WriteInt ( w, data.Revision ); (* 4 *)
			END;
			w.Char ( 0AX ); (* 5 *)
			w.Char ( 0AX ); (* 6 *)
			WriteString ( w, data.Schedule ); (* 7 *)

			IF data.Schedule = "" THEN
				IF data.TextLastUpdated = "" THEN
					(* TODO is this precise enough? maybe we need to get the time from the file directly *)
					Strings.FormatDateTime ( SVNOutput.DateFormat, Dates.Now(), tmp );
					WriteString ( w, tmp ); (* 8 *)
				ELSE
					WriteString ( w, data.TextLastUpdated ); (* 8 *)
				END;
				WriteString ( w, data.Checksum ); (* 9 *)
				WriteString ( w, data.LastChangedDate ); (* 10 *)
				WriteInt ( w, data.LastChangedRevision ); (* 11 *)
				WriteString ( w, data.LastChangedAuthor ); (* 12 *)

				(* TODO more parameters.. like has-props.. add them if needed *)
			END;
		ELSE
			(* write the directory section in .svn/entries *)
			WriteString ( w, data.Name ); (* 2 *)
			w.String ( "dir" ); w.Char ( 0AX ); (* 3 *)

			IF data.Schedule # "" THEN
				w.Char ( 0AX ); (* 4 *)
				w.Char ( 0AX ); (* 5 *)
				w.Char ( 0AX ); (* 6 *)
				WriteString ( w, data.Schedule ); (* 7 *)
			END;
		END;

		w.Char ( CHR(12) ); w.Char ( 0AX );
		w.Update;
	END Write;

	(* write data to the .svn/all-wcprops file *)
	PROCEDURE WriteWCPROPS* ( CONST path, filename, verurl : ARRAY OF CHAR );
	CONST
		key = "svn:wc:ra_dav:version-url";
	VAR
		tmp,fstr,fstr2 : ARRAY 256 OF CHAR;
		read, len, keyLength, i, res : LONGINT;
		fr, fw : Files.File;
		r : Files.Reader;
		w : Files.Writer;
		overwrite, nextFileEntry, hasDirEntry : BOOLEAN;
	BEGIN
		keyLength := Strings.Length ( key );

		Files.JoinPath ( path, ".svn/all-wcprops", fstr );
		Files.JoinPath ( path, ".svn/tmp/od-all-wcprops", fstr2 );

		fw := Files.Old ( fstr );
		IF fw = NIL THEN
			fw := Files.New ( fstr );
			Files.Register ( fw );
		END;

		RemoveFileAttribute2 ( fstr, fw );

		overwrite := TRUE;
		Files.CopyFile ( fstr, fstr2, overwrite, res );
		ASSERT ( res = Files.Ok );

		fr := Files.Old ( fstr2 );
		ASSERT ( fr # NIL );

		Files.OpenWriter ( w, fw, 0 );
		Files.OpenReader ( r, fr, 0 );

		hasDirEntry := FALSE;

		IF filename # "" THEN
			(* we have a file, first search for it and then add it at the correct position *)
			nextFileEntry := FALSE;
			LOOP
				r.Ln ( tmp );
				IF r.res # Files.Ok THEN
					IF ~hasDirEntry THEN
						(* make dummy dir entry *)
						w.String ( "K " ); w.String ( "0" ); w.Char ( 0AX );
						w.String ( "" ); w.Char ( 0AX );
						w.String ( "V " ); w.String ( "0" ); w.Char ( 0AX );
						w.String ( "" ); w.Char ( 0AX );
						w.String ( "END" ); w.Char ( 0AX );
					END;

					(* didn't find the file..so we add a new one *)
					w.String ( filename ); w.Char ( 0AX );
					w.Update;
					EXIT;
				END;

				hasDirEntry := TRUE;
				w.String ( tmp ); w.Char ( 0AX );

				IF nextFileEntry & (tmp = filename) THEN EXIT END;

				nextFileEntry := (tmp = "END");
			END;
		END;

		(* directory entry comes at the beginning *)
		w.String ( "K " ); w.Int ( keyLength, 0 ); w.Char ( 0AX );
		w.String ( key ); w.Char ( 0AX );
		w.String ( "V " ); w.Int ( Strings.Length ( verurl ), 0 ); w.Char ( 0AX );
		w.String ( verurl ); w.Char ( 0AX );
		w.String ( "END" ); w.Char ( 0AX );

		(* search the start position of the remaining entries *)
		FOR i := 1 TO 5 DO
			r.SkipLn;
		END;

		(* copy the rest *)
		read := 0;
		LOOP
			r.Bytes ( tmp, read, LEN(tmp), len );
			w.Bytes ( tmp, read, len );
			INC ( read, len );
			IF len <= LEN(tmp) THEN EXIT END;
		END;

		w.Update;

		SetFileAttribute2 ( fstr, fw );
	END WriteWCPROPS;


	PROCEDURE CreateDirectory* ( CONST path : ARRAY OF CHAR );
	VAR
		tmp, tmp2, svndir : Files.FileName;
		res : LONGINT;
		f : Files.File;
		w : Files.Writer;
	BEGIN
		Files.JoinPath ( path, ".svn", svndir ); Files.CreateDirectory ( svndir, res ); ASSERT ( res = 0 );
		Files.JoinPath ( svndir, "prop-base", tmp ); Files.CreateDirectory ( tmp, res ); ASSERT ( res = 0 );
		Files.JoinPath ( svndir, "props", tmp ); Files.CreateDirectory ( tmp, res ); ASSERT ( res = 0 );
		Files.JoinPath ( svndir, "text-base", tmp ); Files.CreateDirectory ( tmp, res ); ASSERT ( res = 0 );
		Files.JoinPath ( svndir, "tmp", tmp ); Files.CreateDirectory ( tmp, res ); ASSERT ( res = 0 );
		Files.JoinPath ( tmp, "prop-base", tmp2 ); Files.CreateDirectory ( tmp2, res ); ASSERT ( res = 0 );
		Files.JoinPath ( tmp, "props", tmp2 ); Files.CreateDirectory ( tmp2, res ); ASSERT ( res = 0 );
		Files.JoinPath ( tmp, "text-base", tmp2 ); Files.CreateDirectory ( tmp2, res ); ASSERT ( res = 0 );

		Files.JoinPath ( svndir, "format", tmp );
		f := Files.New ( tmp );
		Files.OpenWriter ( w, f, 0 );
		w.Int ( EntryFileFormat, 0 );
		w.Char ( 0AX );
		w.Update;
		Files.Register ( f );

		SetFileAttribute ( tmp );
	END CreateDirectory;


	(* refactor: use SVNAdmin.Read* to get checksum.. *)
	PROCEDURE ReadChecksum* ( CONST file : ARRAY OF CHAR ) : Strings.String;
	VAR
		tmp, path, name : ARRAY 256 OF CHAR;
		nextFileEntry : BOOLEAN;
		f : Files.File;
		r : Files.Reader;
		i : LONGINT;
		s : Strings.String;
		start: ARRAY 2 OF CHAR;
	BEGIN
		NEW ( s, 34 );

		Files.SplitPath ( file, path, name );

		IF Strings.EndsWith ( "svn-base", file ) THEN
			Files.SplitPath ( path, path, tmp );
			Files.SplitPath ( path, path, tmp );
			Strings.Truncate ( name, Strings.Length ( name ) - Strings.Length ( ".svn-base" ) );
		END;

		Files.JoinPath ( path, ".svn/entries", tmp );

		f := Files.Old ( tmp );
		ASSERT ( f # NIL );
		Files.OpenReader ( r, f, 0 );

		nextFileEntry := FALSE;
		LOOP
			r.Ln ( tmp );
			IF r.res # Files.Ok THEN RETURN NIL END;

			IF nextFileEntry & (tmp = name) THEN
				FOR i := 1 TO 6 DO
					r.Ln ( tmp );
					ASSERT ( r.res = Files.Ok );
				END;

				r.Ln ( s^ );
				RETURN s;
			END;
			start[0] := CHR(12); start[1] := 0X;
			nextFileEntry := Strings.StartsWith ( start, 0, tmp );
		END;
	END ReadChecksum;



PROCEDURE CheckChecksum* ( CONST file : ARRAY OF CHAR ) : BOOLEAN;
VAR
	s, s2 : Strings.String;
BEGIN
	s := SVNUtil.GetChecksum ( file );
	s2 := ReadChecksum ( file );
	RETURN s^ = s2^;
END CheckChecksum;


	PROCEDURE Traverse* ( CONST path : ARRAY OF CHAR; handler : TraverseHandler; data : ANY; verurl : BOOLEAN;  VAR res : LONGINT );
	VAR
		tmp, tmp2 : ARRAY 256 OF CHAR;
		adminEntry : Entry;
	BEGIN
		NEW ( adminEntry, NIL );

		adminEntry.SetPath ( path, res );
		IF res # SVNOutput.ResOK THEN RETURN END;

		adminEntry.ReadData ( res );
		IF res # SVNOutput.ResOK THEN RETURN END;

		IF verurl & (adminEntry.adminDir.Schedule = "") THEN
			adminEntry.ReadVersionUrl ( "" );
		END;

		IF adminEntry.pathIsFile THEN
			Files.SplitPath ( path, tmp, tmp2 );
			IF handler ( tmp,adminEntry.adminDir, data ) THEN END;
			RETURN;
		END;

		IF ~handler ( path, adminEntry.adminDir, data ) THEN
			RETURN;
		END;

		WHILE adminEntry.r.Peek() # 0X DO
			adminEntry.ReadFileData ( "", res );
			ASSERT ( res = SVNOutput.ResOK );

			IF adminEntry.adminDir.NodeKind = "dir" THEN
				Files.JoinPath ( path, adminEntry.adminDir.Name, tmp );
				Traverse ( tmp, handler, data, verurl, res );
			ELSE
				IF verurl & (adminEntry.adminDir.Schedule = "") THEN
					adminEntry.ReadVersionUrl ( adminEntry.adminDir.Name );
				END;

				IF ~handler ( path, adminEntry.adminDir, data ) THEN
					RETURN;
				END;
			END;
		END;
	END Traverse;



	PROCEDURE CopyToBaseFile* ( CONST file : ARRAY OF CHAR );
	VAR
		res : LONGINT;
		overwrite : BOOLEAN;
		dest, path, name : Files.FileName;
	BEGIN
		IF SVNUtil.FileExists ( file ) THEN
			overwrite := TRUE;

			Files.SplitPath ( file, path, name );
			Files.JoinPath ( path, ".svn/text-base", path );
			Files.JoinPath ( path, name, dest );
			Strings.Append ( dest, ".svn-base" );

			Kernel32.SetFileAttributes ( dest, {} );
			Files.CopyFile ( file, dest, overwrite, res );
			Kernel32.SetFileAttributes ( dest, {Files.ReadOnly} );
			ASSERT ( res = Files.Ok );
		END;
	END CopyToBaseFile;



	PROCEDURE SetFileAttribute* ( file : ARRAY OF CHAR );
	BEGIN
		Kernel32.SetFileAttributes ( file, {Files.ReadOnly} );
	END SetFileAttribute;

	PROCEDURE RemoveFileAttribute* ( file : ARRAY OF CHAR );
	BEGIN
		Kernel32.SetFileAttributes ( file, {} );
	END RemoveFileAttribute;


	PROCEDURE SetFileAttribute2* ( file : ARRAY OF CHAR; f : Files.File );
	BEGIN
		Kernel32.SetFileAttributes ( file, {Files.ReadOnly} );
		INCL ( f.flags, Files.ReadOnly );
	END SetFileAttribute2;

	PROCEDURE RemoveFileAttribute2* ( file : ARRAY OF CHAR; f : Files.File );
	BEGIN
		Kernel32.SetFileAttributes ( file, {} );
		EXCL ( f.flags, Files.ReadOnly );
	END RemoveFileAttribute2;

END SVNAdmin.