source: trunk/Installers/GitHub/GHInstaller_main.ipf @ 1117

Last change on this file since 1117 was 1117, checked in by ilavsky, 17 months ago

Fix USAXS data ASCII export

  • Property svn:eol-style set to native
File size: 21.5 KB
Line 
1#pragma TextEncoding = "UTF-8"          // For details execute DisplayHelpTopic "The TextEncoding Pragma"
2#pragma rtGlobals=3             // Use modern global access method and strict wave access.
3#pragma version = 1.12
4#pragma IgorVersion = 8.03
5
6
7
8Strconstant ksNameOfPackages ="Irena, Nika, and Indra"
9Strconstant ksWebAddressForConfFile ="https://raw.githubusercontent.com/jilavsky/SAXS_IgorCode/master/"
10Strconstant ksNameOfConfFile ="IgorInstallerConfig.xml"
11strconstant strConstRecordwwwAddress="https://usaxs.xray.aps.anl.gov/staff/jan-ilavsky/IrenaNikaRecords/installrecord.php?"
12Strconstant NameOfInstallMessageFile ="InstallMessage.ifn"
13
14//1.12 fix GH links issue which was failing to loacate proper name for new folder.
15//1.11 critical upgrade, fix for bug in code which relies on bug in Igor behavior which will be fixed in Igor 8.05 and 9
16//1.10 adds ability to delete folders on desktop
17//1.09 adds better unzip for Windows 8 and 10.
18//1.08 added better messages for failed installations.
19//1.05 fix location of new php file, increase Igor version need - rest of the code needs 7.05 or higher anyway.
20//1.05 adds Message from installer, promoted version requirement to 7.05 
21//1.04 Addes recording of installation (method, packages, success etc) for statistical purposes.
22//1.03 updated to handle better failed downloads of files from Github.
23//1.02 Fixes to some paths which were causing issues unzipping files
24//1.0 promoted to 1.0, seems to work.
25//0.6 minor fix of support packages to handle two most common errors.
26//0.5 ready for beta release.
27//0.3 looks like first functioning version on Windows 10.
28
29//Universal installer using Github as source for installtions files.
30//****************************************************************
31//****************************************************************
32//****************************************************************
33Menu "Install Packages"
34        "Open GitHub GUI", GHW_Start()
35        help={"Open GUI to install packages from GitHub"}
36end
37
38//****************************************************************
39//*********************************F*******************************
40//****************************************************************
41//****************************************************************
42Function GHW_Start()
43        if (str2num(stringByKey("IGORVERS",IgorInfo(0)))<7.00)
44                        DoAlert /T="Important message :"  0, "This installer will work ONLY with Igor 7.00 or higher. Please, update your Igor before running this installer!" 
45                        BrowseURL "http://www.wavemetrics.com/support/versions.htm"
46        else
47                DoWIndow GH_MainPanel
48                if(V_Flag)
49                        DoWIndow/K GH_MainPanel
50                endif
51                GHW_InitializeInstaller()
52                GHW_DwnldConfFileAndScanLocal(0)
53                GHW_PrepareGUIData()
54                GHW_CreateMainpanel()
55                GHW_GenerateHelp()
56                GHW_GetAndDisplayUpdateMessage()
57        endif
58
59end
60//****************************************************************
61//****************************************************************
62//****************************************************************
63Function GHW_GetAndDisplayUpdateMessage()
64                //checks for update message and if available, gets it and presents to user.
65               
66        string FileContent
67        string ConfigFileURL=ksWebAddressForConfFile+NameOfInstallMessageFile
68        URLRequest/Z/TIME=2 url=ConfigFileURL
69        if (V_Flag != 0)
70                print "Could not get Install message file from server."
71                return 0
72        endif
73        FileContent =  S_serverResponse
74        variable refNum
75        NewPath/O/C/Q TempUserUpdateMessage, SpecialDirPath("Temporary",0,0,0)
76        Open/P=TempUserUpdateMessage  refNum as NameOfInstallMessageFile
77        FBinWrite refNum, FileContent
78        Close refNum
79        OpenNotebook/k=1/N=MessageFromAuthor/P=TempUserUpdateMessage/Z NameOfInstallMessageFile
80   return 1
81end
82
83//****************************************************************
84//****************************************************************
85//****************************************************************
86
87Function GHW_CreateMainpanel()
88
89        PauseUpdate; Silent 1           // building window...
90        NewPanel /K=1/W=(131,111,733,450) as "Install/Unistall Packages"
91        DoWindow/C GH_MainPanel
92        //ShowTools/A
93        SetDrawLayer UserBack
94        SetDrawEnv fsize= 20,fstyle= 3,textrgb= (1,4,52428)
95        DrawText 101,25,"Install/Uninstall Igor packages hosted on Github"
96        DrawText 11,45,"This Igor experiment enables user to manage "+ksNameOfPackages+" packages"
97        Button CheckVersions,pos={10,50},size={200,20},proc=GHW_ButtonProc,title="Check packages versions"
98        Checkbox DisplayBetaReleases, pos={5,77}, size={100,20}, variable = root:Packages:GHInstaller:DisplayBetaReleases, proc=GHW_CheckProc
99        Checkbox DisplayBetaReleases, help={"Check to display beta releases?"}, title="  Include Beta releases in list?"
100        Checkbox UseLocalFolder, pos={250,50}, size={100,20}, variable = root:Packages:GHInstaller:UseLocalFolder, proc=GHW_CheckProc
101        Checkbox UseLocalFolder, help={"Check to use Local Folder?"}, title="  Use Local folder?"
102        PopupMenu SelectReleaseToInstall,pos={233.00,75.00},size={231.00,23.00},bodyWidth=120,proc=GHW_PopMenuProc,title="Select Release to Install:"
103        PopupMenu SelectReleaseToInstall,help={"Select release to install"}
104        PopupMenu SelectReleaseToInstall,mode=1,value= #"root:Packages:GHInstaller:PopListOfReleaseNames"
105        SetVariable ReleaseNotes variable = root:Packages:GHInstaller:ReleaseNotes
106        SetVariable ReleaseNotes pos={10,98},size={570,18},disable=2
107        ListBox InstallationSelection,pos={10.00,120.00},size={362.00,177.00}
108        ListBox InstallationSelection,listWave=root:Packages:GHInstaller:VersionsAndInstall
109        ListBox InstallationSelection,selWave=root:Packages:GHInstaller:SelVersionsAndInstall
110        ListBox InstallationSelection,mode= 8,userColumnResize= 1
111
112        Button GetHelp,pos={480,45},size={110,20},proc=GHW_ButtonProc,title="Get Help"
113        Button InstallPackages,pos={390,126},size={200,20},proc=GHW_ButtonProc,title="Install/Update Selected", fColor=(16386,65535,16385)
114        Button UninstallPackages,pos={390,155},size={200,20},proc=GHW_ButtonProc,title="Uninstall Selected", fColor=(16386,65535,16385)
115
116        Button OpenWebSIte,pos={390,200},size={200,18},proc=GHW_ButtonProc,title="Ilavsky Web site"
117        Button OpenGitHub,pos={390,230},size={200,18},proc=GHW_ButtonProc,title="Github depository"
118        Button SignupIrena,pos={390,260},size={200,18},proc=GHW_ButtonProc,title="Sign up for Irena mailing list"
119        Button SignUpNika,pos={390,290},size={200,18},proc=GHW_ButtonProc,title="Sign up for Nika mailing list"
120//
121        DrawText 5,320,"Version 1.11 of Github Installer, JIL."
122        DrawText 5,335,"Please, check the web site for latest version before using."
123end
124//****************************************************************
125//****************************************************************
126//**************************************************************** //****************************************************************
127//****************************************************************
128//****************************************************************
129
130Function GHW_CheckProc(cba) : CheckBoxControl
131        STRUCT WMCheckboxAction &cba
132       
133        NVAR DisplayBetaReleases = root:Packages:GHInstaller:DisplayBetaReleases
134
135        switch( cba.eventCode )
136                case 2: // mouse up
137                        Variable checked = cba.checked
138                        if(stringmatch(cba.ctrlName,"DisplayBetaReleases"))
139                                GHW_PrepareGUIData()
140                                //Inst_CheckForLocalCopyPresence()
141                        endif
142                        if(stringmatch(cba.ctrlName,"UseLocalFolder"))
143                                SVAR LocalFolderPath    =       root:Packages:GHInstaller:LocalFolderPath
144                                NVAR UseLocalFolder     =       root:Packages:GHInstaller:UseLocalFolder
145                                NVAR DisplayBetaReleases = root:Packages:GHInstaller:DisplayBetaReleases
146                                if(checked)
147                                        PathInfo/S userDesktop
148                                        NewPath /M="Select Location of Folder with data downloaded from GitHub"  /O/Q LocalInstallationFolder 
149                                        PathInfo LocalInstallationFolder
150                                        LocalFolderPath = S_path
151                                        DisplayBetaReleases = 0
152                                else
153                                        KillPath/Z LocalInstallationFolder
154                                endif
155                                GHW_DwnldConfFileAndScanLocal(1)
156                                GHW_PrepareGUIData()
157                                //Inst_CheckForLocalCopyPresence()
158                        endif
159                        break
160                case -1: // control being killed
161                        break
162        endswitch
163
164        return 0
165End
166
167//****************************************************************
168//****************************************************************
169//****************************************************************
170//****************************************************************
171
172Function GHW_ButtonProc(ba) : ButtonControl
173        STRUCT WMButtonAction &ba
174
175        switch( ba.eventCode )
176                case 2: // mouse up
177                        //check Igor Pro version and bail out if not higher than 7.00
178                        string IgorInfoStr=IgorInfo(0)
179                        if(IgorVersion()<7.00)
180                                Abort "This code requires Igor 7.00 or higher, please update your Igor Pro."
181                        endif
182                       
183                        if(stringMatch(ba.ctrlName,"CheckVersions"))
184                                GHW_DwnldConfFileAndScanLocal(1)
185                                GHW_PrepareGUIData()
186                        endif
187
188                        if(stringMatch(ba.ctrlName,"InstallPackages")) 
189                                if(GHW_IsThereWhatToDo())
190                                        GHW_Install()   
191                                        GHW_DwnldConfFileAndScanLocal(1)
192                                        GHW_PrepareGUIData()
193                                else
194                                        DoALert 0, "Nothing to do, select some packages to install"
195                                endif
196                        endif
197                       
198                        if(stringMatch(ba.ctrlName,"OpenWebSite"))                     
199                                BrowseURL "http://usaxs.xray.aps.anl.gov/staff/ilavsky/index.html"
200                        endif
201                        if(stringMatch(ba.ctrlName,"OpenGitHub"))                       
202                                BrowseURL "https://github.com/jilavsky/SAXS_IgorCode"
203                        endif
204                        if(stringMatch(ba.ctrlName,"SignupIrena"))                     
205                                BrowseURL "http://www.aps.anl.gov/mailman/listinfo/irena_users"
206                        endif
207                        if(stringMatch(ba.ctrlName,"SignUpNika"))                       
208                                BrowseURL "http://www.aps.anl.gov/mailman/listinfo/nika_users"
209                        endif
210
211
212                        if(stringMatch(ba.ctrlName,"UninstallPackages"))
213                                if(GHW_IsThereWhatToDo())
214                                        GHW_Uninstall()
215                                        GHW_DwnldConfFileAndScanLocal(1)
216                                        GHW_PrepareGUIData()
217                                else
218                                        DoALert 0, "Nothing to do, select some packages to uninstall"
219                                endif
220                        endif
221                                               
222                        if(stringMatch(ba.ctrlName,"GetHelp"))
223                                GHW_GenerateHelp()
224                        endif
225                       
226                        break
227        endswitch
228
229        return 0
230End
231//****************************************************************
232Function GHW_IsThereWhatToDo()
233        Wave SelVersionsAndInstall = root:Packages:GHInstaller:SelVersionsAndInstall   
234        variable NumPckgs = DimSize(SelVersionsAndInstall, 0 )
235        variable i, result
236        result = 0
237        for(i=0;i<NumPckgs;i+=1)
238                result += SelVersionsAndInstall[i][3] > 32 ? 1 : 0
239        endfor
240       
241        return result
242
243end//****************************************************************
244//****************************************************************
245Function GHW_PopMenuProc(pa) : PopupMenuControl
246        STRUCT WMPopupAction &pa
247
248        switch( pa.eventCode )
249                case 2: // mouse up
250                        Variable popNum = pa.popNum
251                        String popStr = pa.popStr
252                        if(StringMatch(pa.ctrlName, "SelectReleaseToInstall"))
253                                //fix something here...
254                                SVAR SelectedReleaseName = root:Packages:GHInstaller:SelectedReleaseName
255                                SelectedReleaseName = popStr
256                                GHW_PrepareListboxGUIData()
257                        endif
258                        break
259                case -1: // control being killed
260                        break
261        endswitch
262
263        return 0
264End
265
266//****************************************************************
267//****************************************************************
268//****************************************************************
269//****************************************************************
270//****************************************************************
271static Function GHW_ListIgorProcFiles()
272        GetFileFolderInfo/Q/Z/P=Igor "Igor Procedures" 
273        if(V_Flag==0)
274                GHW_ListProcFiles(S_Path,1 )
275        endif
276        GetFileFolderInfo/Q/Z GHW_GetIgorUserFilesPath()+"Igor Procedures:"
277        if(V_Flag==0)
278                GHW_ListProcFiles(GHW_GetIgorUserFilesPath()+"Igor Procedures:",0)
279        endif
280        KillPath/Z tempPath
281end
282 //****************************************************************
283//****************************************************************
284 //****************************************************************
285//****************************************************************
286//****************************************************************
287//****************************************************************
288//****************************************************************
289//****************************************************************
290static Function GHW_ListIgorExtensionsFiles()
291        GetFileFolderInfo/Q/Z/P=Igor "Igor Extensions" 
292        if(V_Flag==0)
293                GHW_ListProcFiles(S_Path, 1)
294        endif
295        GetFileFolderInfo/Q/Z (GHW_GetIgorUserFilesPath()+"Igor Extensions:")
296        if(V_Flag==0)
297                GHW_ListProcFiles(GHW_GetIgorUserFilesPath()+"Igor Extensions:",0)
298        endif
299        KillPath/Z tempPath
300end
301
302//****************************************************************
303//****************************************************************
304static Function /S GHW_Windows2IgorPath(pathIn)
305        String pathIn
306        String pathOut = ParseFilePath(5, pathIn, ":", 0, 0)
307        return pathOut
308End
309
310static Function/S GHW_GetIgorUserFilesPath()
311        // This should be a Macintosh path but, because of a bug prior to Igor Pro 6.20B03
312        // it may be a Windows path.
313        String path = SpecialDirPath("Igor Pro User Files", 0, 0, 0)
314        path = GHW_Windows2IgorPath(path)
315        return path
316End
317//****************************************************************
318//****************************************************************
319//****************************************************************
320//****************************************************************
321static Function GHW_ListProcFiles(PathStr, resetWaves)
322        string PathStr
323        variable resetWaves
324       
325        String abortMessage     //HR Used if we have to abort because of an unexpected error
326       
327        string OldDf=GetDataFolder(1)
328        //create location for the results waves...
329        NewDataFolder/O/S root:Packages
330        NewDataFolder/O/S root:Packages:UseProcedureFiles
331        //if this is top call to the routine we need to wipe out the waves so we remove old junk
332        string CurFncName=GetRTStackInfo(1)
333        string CallingFncName=GetRTStackInfo(2)
334        variable runningTopLevel=0
335        if(!stringmatch(CurFncName,CallingFncName))
336                runningTopLevel=1
337        endif
338        if(resetWaves)
339                        Make/O/N=0/T FileNames         
340                        Make/O/N=0/T PathToFiles
341                        Make/O/N=0 FileVersions
342        endif
343       
344       
345        //if this was first call, now the waves are gone.
346        //and now we need to create the output waves
347        Wave/Z/T FileNames
348        Wave/Z/T PathToFiles
349        Wave/Z FIleVersions
350        If(!WaveExists(FileNames) || !WaveExists(PathToFiles) || !WaveExists(FIleVersions))
351                Make/O/T/N=0 FileNames, PathToFIles
352                Make/O/N=0 FileVersions
353                Wave/T FileNames
354                Wave/T PathToFiles
355                Wave FileVersions
356                //I am not sure if we really need all of those declarations, but, well, it should not hurt...
357        endif
358        string str
359        //this is temporary path to the place we are looking into now... 
360        NewPath/Q/O tempPath, PathStr
361        if (V_flag != 0)                //HR Add error checking to prevent infinite loop
362                sprintf abortMessage, "Unexpected error creating a symbolic path pointing to \"%s\"", PathStr
363                str= abortMessage       // To make debugging easier
364                //Inst_Append2Log(str,0)
365                Abort abortMessage
366        endif
367
368        //list al items in this path
369        string ItemsInTheFolder= IndexedFile(tempPath,-1,"????")+IndexedDir(tempPath, -1, 0 )   
370        //HR If there is a shortcut in "Igor Procedures", ItemsInTheFolder will include something like "HDF5 Browser.ipf.lnk". Windows shortcuts are .lnk files.       
371        //remove all . files.
372        ItemsInTheFolder = GrepList(ItemsInTheFolder, "^\." ,1)
373        //Now we removed all junk files on Macs (starting with .)
374        //now lets check what each of these files are and add to the right lists or follow...
375        variable i, imax=ItemsInList(ItemsInTheFolder)
376        string tempFileName, tempScraptext, tempPathStr
377        variable IamOnMac, isItXOP
378        if(stringmatch(IgorInfo(2),"Windows"))
379                IamOnMac=0
380        else
381                IamOnMac=1
382        endif
383        For(i=0;i<imax;i+=1)
384                tempFileName = stringfromlist(i,ItemsInTheFolder)
385                GetFileFolderInfo/Z/Q/P=tempPath tempFileName
386                isItXOP = IamOnMac * stringmatch(tempFileName, "*xop*" )
387               
388                if(V_isAliasShortcut)
389                        //HR If tempFileName is "HDF5 Browser.ipf.lnk", or any other shortcut to a file, S_aliasPath is a path to a file, not a folder.
390                        //HR Thus the "NewPath tempPath" command will fail.
391                        //HR Thus tempPath will retain its old value, causing you to recurse the same folder as before, resulting in an infinite loop.
392                       
393                        //is alias, need to follow and look further. Use recursion...
394                        if(strlen(S_aliasPath)>3)               //in case user has stale alias, S_aliasPath has 0 length. Need to skip this pathological case.
395                                //HR Recurse only if S_aliasPath points to a folder. I don't really know what I'm doing here but this seems like it will prevent the infinite loop.
396                                GetFileFolderInfo/Z/Q/P=tempPath S_aliasPath   
397                                isItXOP = IamOnMac * stringmatch(S_aliasPath, "*xop*" )
398                                if (V_flag==0 && V_isFolder&&!isItXOP)          //this is folder, so all items in the folder are included... Except XOP is folder too...
399                                        GHW_ListProcFiles(S_aliasPath, 0)
400                                elseif(V_flag==0 && (!V_isFolder || isItXOP))   //this is link to file. Need to include the info on the file...
401                                        //*************
402                                        Redimension/N=(numpnts(FileNames)+1) FileNames, PathToFiles,FileVersions
403                                        tempFileName =stringFromList(ItemsInList(S_aliasPath,":")-1, S_aliasPath,":")
404                                        tempPathStr = RemoveFromList(tempFileName, S_aliasPath,":")
405                                        FileNames[numpnts(FileNames)-1] = tempFileName
406                                        PathToFiles[numpnts(FileNames)-1] = tempPathStr
407                                        //try to get version from #pragma version = ... This seems to be the most robust way I found...
408                                        NewPath/Q/O tempPath, tempPathStr
409                                        if(stringmatch(tempFileName, "*.ipf"))
410                                                Grep/P=tempPath/E="(?i)^#pragma[ ]*version[ ]*=[ ]*" tempFileName as "Clipboard"
411                                                sleep/s (0.02)
412                                                tempScraptext = GetScrapText()
413                                                if(strlen(tempScraptext)>10)            //found line with #pragma version"
414                                                        tempScraptext = replaceString("#pragma",tempScraptext,"")       //remove #pragma
415                                                        tempScraptext = replaceString("version",tempScraptext,"")               //remove version
416                                                        tempScraptext = replaceString("=",tempScraptext,"")                     //remove =
417                                                        tempScraptext = replaceString("\t",tempScraptext,"  ")                  //remove optional tabulators, some actually use them.
418                                                        tempScraptext = removeending(tempScraptext," \r")                       //remove optional tabulators, some actually use them.
419                                                        //forget about the comments behind the text.
420                                                       //str2num is actually quite clever in this and converts start of the string which makes sense.
421                                                        FileVersions[numpnts(FileNames)-1]=str2num(tempScraptext)
422                                                else             //no version found, set to NaN
423                                                        FileVersions[numpnts(FileNames)-1]=NaN
424                                                endif
425                                        else                    //no version for non-ipf files
426                                                FileVersions[numpnts(FileNames)-1]=NaN
427                                        endif
428                                        //************
429                                endif
430                        endif
431                        //and now when we got back, fix the path definition to previous or all will crash...
432                        NewPath/Q/O tempPath, PathStr
433                        if (V_flag != 0)                //HR Add error checking to prevent infinite loop
434                                sprintf abortMessage, "Unexpected error creating a symbolic path pointing to \"%s\"", PathStr
435                                str= abortMessage       // To make debugging easier
436                        //      Inst_Append2Log(str,0)
437                                Abort abortMessage
438                        endif
439                elseif(V_isFolder&&!isItXOP)   
440                        //is folder, need to follow into it. Use recursion.
441                        GHW_ListProcFiles(PathStr+tempFileName+":", 0)
442                        //and fix the path back or all will fail...
443                        NewPath/Q/O tempPath, PathStr
444                        if (V_flag != 0)                //HR Add error checking to prevent infinite loop
445                                sprintf abortMessage, "Unexpected error creating a symbolic path pointing to \"%s\"", PathStr
446                                str= abortMessage       // To make debugging easier
447                        //      Inst_Append2Log(str,0)
448                                Abort abortMessage
449                        endif
450                elseif(V_isFile||isItXOP)
451                        //this is real file. Store information as needed.
452                        Redimension/N=(numpnts(FileNames)+1) FileNames, PathToFiles,FileVersions
453                        FileNames[numpnts(FileNames)-1] = tempFileName
454                        PathToFiles[numpnts(FileNames)-1] = PathStr
455                        //try to get version from #pragma version = ... This seems to be the most robust way I found...
456                        if(stringmatch(tempFileName, "*.ipf"))
457                                Grep/P=tempPath/E="(?i)^#pragma[ ]*version[ ]*=[ ]*" tempFileName as "Clipboard"
458                                sleep/s(0.02)
459                                tempScraptext = GetScrapText()
460                                if(strlen(tempScraptext)>10)            //found line with #pragma version"
461                                        tempScraptext = replaceString("#pragma",tempScraptext,"")       //remove #pragma
462                                        tempScraptext = replaceString("version",tempScraptext,"")               //remove version
463                                        tempScraptext = replaceString("=",tempScraptext,"")                     //remove =
464                                        tempScraptext = replaceString("\t",tempScraptext,"  ")                  //remove optional tabulators, some actually use them.
465                                        //forget about the comments behind the text.
466                                       //str2num is actually quite clever in this and converts start of the string which makes sense.
467                                        FileVersions[numpnts(FileNames)-1]=str2num(tempScraptext)
468                                else             //no version found, set to NaN
469                                        FileVersions[numpnts(FileNames)-1]=NaN
470                                endif
471                        else                    //no version for non-ipf files
472                                FileVersions[numpnts(FileNames)-1]=NaN
473                        endif
474                endif
475        endfor
476        setDataFolder OldDf
477end
478
479
480//***********************************
481//***********************************
482//***********************************
483//***********************************
484
485//static
486Function GHW_FileFolderExists(name,[path,file,folder])  // returns 1=exists, 0=does not exist
487        String name                                     // partial or full file name or folder name
488        String path                                     // optional path name, e.g. "home"
489        Variable file,folder    // flags, if both set or both unset, it checks for either
490        path = SelectString(ParamIsDefault(path),path,"")
491        file = ParamIsDefault(file) ? 0 : file
492        file = numtype(file) ? 0 : !(!file)
493        folder = ParamIsDefault(folder) ? 0 : folder
494        folder = numtype(folder) ? 0 : !(!folder)
495
496        if (!file && !folder)   // check for either
497                file = 1
498                folder = 1
499        endif
500
501        if (strlen(path))
502                PathInfo $path
503                if (V_flag==0)
504                        return 0
505                endif
506                name = S_path+name      // add the path to name
507        endif
508
509        GetFileFolderInfo/Q/Z=1 name
510        Variable found=0
511        found = found || (file ? V_isFile : 0)
512        found = found || (folder ? V_isFolder : 0)
513        return found
514End
Note: See TracBrowser for help on using the repository browser.