mercurial/HGCommandParser.st
author Jan Vrany <jan.vrany@fit.cvut.cz>
Thu, 07 Mar 2019 12:22:12 +0000
changeset 867 7527dc6bc38e
parent 866 8a885a75daa9
child 927 2c1315a145fd
permissions -rw-r--r--
Issue 256: fix parsing branch list when branch name(s) contains spaces Since branch names may contain space, we must use custom template to get a a robust machine readable output. However, this is supported since Mercurial 3.5 (including) so for older Mercurials, we still use old code as a courtesy to users who may have old Mercurials. Las lomng as branch has no spaces, it should just work.

"
stx:libscm - a new source code management library for Smalltalk/X
Copyright (C) 2012-2015 Jan Vrany

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License. 

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"
"{ Package: 'stx:libscm/mercurial' }"

"{ NameSpace: Smalltalk }"

Object subclass:#HGCommandParser
	instanceVariableNames:'stream command'
	classVariableNames:''
	poolDictionaries:''
	category:'SCM-Mercurial-Internal'
!

!HGCommandParser class methodsFor:'documentation'!

copyright
"
stx:libscm - a new source code management library for Smalltalk/X
Copyright (C) 2012-2015 Jan Vrany

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License. 

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"
! !

!HGCommandParser class methodsFor:'instance creation'!

for: anHGCommand on: aStringOrStream
    | stream |

    stream := aStringOrStream isStream 
                ifTrue:[aStringOrStream]
                ifFalse:[aStringOrStream readStream].

    ^self new
        command: anHGCommand;
        stream: stream;
        yourself

    "Created: / 04-02-2013 / 13:54:37 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

on: aStringOrStream
    ^self for: nil on: aStringOrStream

    "Created: / 23-10-2012 / 11:07:52 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 04-02-2013 / 13:55:18 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser class methodsFor:'templates'!

templateBranches
    ^ '{branch}\0{rev}:{node}\0{ifeq(closed,''True'',''C'',ifeq(active,''False'',''I'',''A''))}\n'

    "Created: / 07-03-2019 / 11:02:46 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

templateHeads
    ^'{rev}:{node}\n'

    "Created: / 27-11-2012 / 21:25:13 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

templateLog
    ^ '{rev}:{node}\n{branch}\n{p1rev}:{p1node} {p2rev}:{p2node} \n\n{file_adds}\n{file_copies}\n{file_dels}\n{file_mods}\n{author}\n{date|isodate}\n{desc}\n**EOE**\n'

    "Created: / 12-11-2012 / 23:06:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 22-11-2014 / 00:22:42 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

templateLogChildren
    ^ '{rev}:{node}\n{children}\n'

    "Created: / 05-12-2012 / 23:40:57 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

templateLogForVersionLessThan2_4
    ^ '{rev}:{node}\n{branch}\n{parents}\n\n{file_adds}\n{file_copies}\n{file_dels}\n{file_mods}\n{author}\n{date|isodate}\n{desc}\n**EOE**\n'

    "Created: / 27-11-2014 / 23:20:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

templateLogIdsOnly
    ^ '{rev}:{node}\n'

    "Created: / 05-12-2012 / 19:10:28 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'accessing'!

command
    ^ command
!

command:anHGCommand
    command := anHGCommand.
!

stream
    ^ stream
!

stream:something
    stream := something.
! !

!HGCommandParser methodsFor:'error reporting'!

error: aString
    <context: #return>
    <resource: #skipInDebuggersWalkBack>

    self propagate: HGCommandParseError message: aString

    "Created: / 14-11-2012 / 19:59:50 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 04-02-2013 / 21:50:30 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

notify: aString
    <context: #return>
    <resource: #skipInDebuggersWalkBack>

    self propagate: HGNotification message: aString

    "Created: / 04-02-2013 / 13:56:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 04-02-2013 / 21:50:42 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

propagate: anException
    "Propagates given exception to the caller og HGCommand>>execute
     (but only if command is set)"

    command notNil ifTrue:[
        command propagate: anException.
        anException isError ifTrue:[
            Processor activeProcess terminate
        ].
    ] ifFalse:[
        anException raiseSignal
    ].

    "Created: / 04-02-2013 / 21:38:53 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 19-03-2014 / 23:30:46 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

propagate: class message: message
    "Propagates an exception of given class with given message to
     the caller of HGCommand>>execute"

    ^self propagate: (class newException messageText: message)

    "Created: / 04-02-2013 / 21:50:13 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

warn: aString
    <context: #return>
    <resource: #skipInDebuggersWalkBack>

    self propagate: HGWarning message: aString

    "Created: / 04-02-2013 / 13:56:35 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 04-02-2013 / 21:50:50 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing'!

parseBookmarks
    "Parse output of 'hg bookmarks' command. Return collection
     of orphaned HGBookmark"

    | bookmarks |

    bookmarks := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        bookmarks add: self parseBookmarksEntry
    ].
    ^bookmarks

    "Created: / 20-03-2014 / 01:51:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseBookmarksEntry
    | bookmark |

    "Example:
       issue37-update-to-revision 399:858944cebec4
     * master                    403:5cc256ed28a1 
    "

    bookmark := HGBookmark new.


    stream skipSeparators.
    stream peek == $* ifTrue:[
        stream next.
        stream skipSeparators.
    ].
    bookmark setName: self parseName.
    stream skipSeparators.
    bookmark setChangesetId: self parseNodeId.
    self expectLineEnd.
    ^bookmark

    "Created: / 20-03-2014 / 01:52:56 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 20-03-2014 / 17:12:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseBranches
    "Parse output of 'hg branches' command. Return collection
     of orphaned HGBranch"
    
    | branches  branch |

    branches := OrderedCollection new.
    stream atEnd ifFalse:[
        branch := self 
                        parseBranchesEntryAllowForInvalidBranchheadsMessageEntryAllowForInvalidBranchheadsMessage:true.
        branch notNil ifTrue:[
            branches add:branch.
        ].
    ].
    [ stream atEnd ] whileFalse:[
        branch := self 
                        parseBranchesEntryAllowForInvalidBranchheadsMessageEntryAllowForInvalidBranchheadsMessage:false.
        branches add:branch.
    ].
    ^ branches

    "Created: / 09-03-2019 / 08:25:55 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseBranchesEntryAllowForInvalidBranchheadsMessageEntryAllowForInvalidBranchheadsMessage:allowForInvalidBrancheadsMessage 
    | name  branch  state |

    name := self parseNameUpToNullOrNewLine.
    allowForInvalidBrancheadsMessage ifTrue:[
        stream peek == Character null ifFalse:[
            | message |

            message := name , ' ' , stream nextLine.
            self notify:message.
            name := self parseNameUpToNullOrNewLine.
        ].
    ].
    branch := HGBranch new.
    branch setName:name.
    self expect:Character null.
    self parseNodeId.
    self expect:Character null.
    state := stream next.
    state == $A ifTrue:[
        branch setActive:true
    ] ifFalse:[
        state == $C ifTrue:[
            branch setClosed:true
        ] ifFalse:[
            state == $I ifTrue:[
                branch setActive:false
            ] ifFalse:[
                self error:'Invalid branch state: ' , state
            ]
        ]
    ].
    self expectLineEnd.
    ^ branch

    "Created: / 09-03-2019 / 08:26:01 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseBranchesOld
    "Parse output of 'hg branches' command. Return collection
     of orphaned HGBranch"
    
    | branches  branch |

    branches := OrderedCollection new.
    stream atEnd ifFalse:[
        branch := self parseBranchesOldEntryAllowForInvalidBranchheadsMessage:true.
        branch notNil ifTrue:[
            branches add:branch.
        ].
    ].
    [ stream atEnd ] whileFalse:[
        branch := self parseBranchesOldEntryAllowForInvalidBranchheadsMessage:false.
        branches add:branch.
    ].
    ^ branches

    "Created: / 27-11-2012 / 20:20:56 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 08-10-2014 / 20:56:29 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseBranchesOldEntryAllowForInvalidBranchheadsMessage:allowForInvalidBrancheadsMessage 
    | name  branch |

    name := self parseName.
    stream skipSeparators.
    allowForInvalidBrancheadsMessage ifTrue:[
        stream peek isDigit ifFalse:[
            | message |

            message := name , ' ' , stream nextLine.
            self notify:message.
            name := self parseName.
            stream skipSeparators.
        ].
    ].
    branch := HGBranch new.
    branch setName:name.
    self parseNodeId.
    stream peek == Character space ifTrue:[
        stream next.
        stream peek == $( ifFalse:[
            self error:'''('' expected but ''' , stream peek , ''' found'
        ].
        stream next.
        stream peek == $i ifTrue:[
            self expect:'inactive)'.
            branch setActive:false.
        ] ifFalse:[
            stream peek == $c ifTrue:[
                self expect:'closed)'.
                branch setClosed:true.
            ] ifFalse:[
                self 
                    error:'Unexpected branch attribute (only ''closed'' and ''inactive'' supported)'''
            ]
        ].
    ].
    self expectLineEnd.
    ^ branch

    "Created: / 08-10-2014 / 20:54:46 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseConfig
    "Parse output of 'hg showconfig' command, assuming the template given
     was HGCommandParser templateLog. Return a list of HGChangeset."

    | root |

    root := HGConfig::Section new setName: '<root>'.
    [ stream atEnd ] whileFalse:[
        self parseConfigEntryInto: root.
    ].
    ^root

    "Created: / 06-12-2012 / 16:00:38 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-12-2012 / 19:59:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseConfigEntryInto: root
    | entry out |

    entry := root.
    out := String new writeStream.
    [ stream atEnd or:[stream peek == $=] ] whileFalse:[
        stream peek == $. ifTrue:[
            entry := entry at: out contents ifAbsentPut: [
                HGConfig::Section new setName: out contents.
            ].
            out reset.
            stream next.
        ] ifFalse:[
            out nextPut: stream next.
        ].
    ].
    stream next.
    entry at: out contents put:
        (HGConfig::Entry new setName: out contents value:stream nextLine)

    "Created: / 06-12-2012 / 19:41:13 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseDate
    | ts c |
    ts := Timestamp readIso8601FormatFrom:stream.
    c := stream peek.
    c == Character space ifTrue:[ 
        stream next.
        c := stream peek.
    ].
    (c == $+ or:[c == $-]) ifFalse:[
        self error:'Cannot read timezone: ''+'' or ''-'' expected, ''' , c , ''' found'
    ].
    stream next.
    4 timesRepeat:[
        ('0123456789' includes: (c := stream peek)) ifFalse:[
            self error:'Cannot read timezone: digit expected, ''' , c , ''' found'
        ].
        stream next.
    ].
    ^ts

    "Created: / 13-11-2012 / 10:22:46 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-11-2014 / 10:44:49 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseInteger
    "Parses integer from stream and returns it"

    ^Integer readFrom: stream onError:[self error: 'integer value expected']

    "
    (HGCommandParser on: '12 34' readStream) parseInteger; skipSeparators; parseInteger
    "

    "Created: / 19-11-2012 / 20:05:28 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseLog
    "Parse output of 'hg log' command, assuming the template given
     was HGCommandParser templateLog. Return a list of HGRevision."

    | revs |

    revs := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        | rev |

        rev := self parseLogEntry.
        revs add: rev.
    ].

    ^revs.

    "Created: / 13-11-2012 / 09:46:28 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseLogEntry
    "Parse single revision entry, assuming the template given
     was HGCommandParser templateLog. Return a HGRevision."

    | rev branches line message adds copies deletions modifications |

    rev := HGChangeset new.
    rev setId: self parseNodeId. self expectLineEnd.
    branches := Array with: stream nextLine.
    rev setBranches: branches.
    rev setParent1Id: self parseNodeId. self expectSpace.
    rev setParent2Id: self parseNodeId. self expectSpace. self expectLineEnd.
    "rev setChildren: self parseNodeIdList." self expectLineEnd.

    adds := self parsePathList. self expectLineEnd.
    copies := self parsePathCopyList. self expectLineEnd.
    deletions := self parsePathList. self expectLineEnd.
    modifications := self parsePathList. self expectLineEnd.

    copies pairsDo:[:dst :src|
        adds remove: dst ifAbsent:[].
        deletions remove: src ifAbsent:[].
    ].

    adds := adds collect:[:e|HGChange newAdded setChangeset: rev path: e].
    copies := copies collect:[:e|HGChange newCopied setChangeset: rev path: e first; setSource: e second].
    deletions := deletions collect:[:e|HGChange newRemoved setChangeset: rev path: e].
    modifications := modifications collect:[:e|HGChange newModified setChangeset: rev path: e].

    rev setChanges: modifications , adds , deletions , copies.
    rev setAuthor: self nextLine.
    rev setTimestamp: self parseDate. self expectLineEnd.
    message := String streamContents:[:s|
        line := self nextLine.
        s nextPutAll: line.
        [ line := self nextLine . line = '**EOE**' ] whileFalse:[
            s cr.
            s nextPutAll: line
        ].
    ].
    rev setMessage: message.
    rev setNonLazy.

    ^rev

    "Created: / 13-11-2012 / 09:45:40 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 07-01-2019 / 22:51:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseMergeLocalChanged: info
    "Parses

    local changed lcmake.bat which remote deleted
    use (c)hanged version or (d)elete? c

    " 

    self expect: 'local changed'.
    stream nextLine.
    self expect: 'use'.
    stream nextLine.

    "Created: / 22-03-2013 / 08:59:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseMergePath: info
    "Parses 'merging Make.proto' line" 

    self expect: 'merging '.
    self parsePath.
    self expectLineEnd.

    "Created: / 14-01-2013 / 15:56:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseMergeRemoteChanged: info
    "Parses

    remote changed CharacterEncoderImplementations__SJIS.st which local deleted
    use (c)hanged version or leave (d)eleted? c

    " 

    self expect: 'remote changed'.
    stream nextLine.
    self expect: 'use'.
    stream nextLine.

    "Created: / 15-01-2013 / 09:59:39 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseMergeSummary
    ^self parseMergeSummary: HGMergeInfo new

    "Created: / 14-01-2013 / 15:48:14 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseMergeSummary: info
    "Example:

        '9 files updated, 0 files merged, 1 files removed, 0 files unresolved'
    "

    info setNumUpdated: self parseInteger.
    self expect: ' files updated, '.
    info setNumMerged: self parseInteger.
    self expect: ' files merged, '.
    info setNumRemoved: self parseInteger.
    self expect: ' files removed, '.
    info setNumUnresolved: self parseInteger.
    self expect: ' files unresolved'.
    self expectLineEnd.
    ^info

    "Created: / 14-01-2013 / 15:52:47 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseName
    ^String streamContents:[:out|
        [ stream peek isSeparator ] whileFalse:[
            out nextPut:stream next
        ]
    ].

    "Created: / 27-11-2012 / 20:21:27 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 07-03-2019 / 11:17:52 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNameList
    | list |

    stream atEnd ifTrue:[ ^#() ].
    stream peek isSeparator ifTrue:[ ^#() ].
    list := OrderedCollection new.
    list add: self parseName.
    [ stream atEnd not and:[stream peek == Character space]] whileTrue:[
        stream next. "/eat space.
        list add: self parseName.
    ].
    ^list.

    "Created: / 27-11-2012 / 20:30:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNameUpTo: terminator1 or: terminator2
    ^String streamContents:[:out|
        | c |    
        [ c := stream peek. c == terminator1 or:[ c == terminator2] ] whileFalse:[
            out nextPut:stream next
        ]
    ].

    "Created: / 07-03-2019 / 12:16:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNameUpToNullOrNewLine
    ^ self parseNameUpTo: Character null or: Character cr.

    "Created: / 07-03-2019 / 12:16:53 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNameZ    
    ^String streamContents:[:out|
        [ stream peek == Character null ] whileFalse:[
            out nextPut:stream next
        ]
    ].

    "Created: / 07-03-2019 / 11:18:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNodeId
    "Parses node id from stream and returns it. Support both,
     short and full node ids"

    ^HGChangesetId readFrom: stream onError:[:msg|self error: msg]



    "
        (HGCommandParser on: '4:6f88e1f44d9eb86e0b56ca15e30e5d786acd83c7' readStream) parseNodeId

        Bad ones:

        (HGCommandParser on: '4:6f88e1f44d9eb86e0b56ca15e30e5d786acd' readStream) parseNodeId
        (HGCommandParser on: '4:6f88Z1f44d9eb86e0b56ca15e30e5d786acd83c7' readStream) parseNodeId

    "

    "Created: / 13-11-2012 / 10:22:49 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 13-11-2012 / 16:52:23 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseNodeIdList
    "Parses node id list from stream and returns it. Support both,
     short and full node ids."

    | ids |

    stream atEnd ifTrue:[ ^ #() ].
    stream peek == Character cr ifTrue:[ ^ #() ].
    ids := OrderedCollection new.
    [ stream peek ~~ Character cr ] whileTrue:[
        ids add: self parseNodeId.
        stream peek == Character space ifTrue:[stream next].
    ].
    ^ids

    "Created: / 05-12-2012 / 17:24:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePath
    "Parse single path entry from repository"

    ^self parseName

    "Created: / 05-12-2012 / 18:27:29 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePathCopy
    "Parse single path entry from repository"

    | dst src |

    dst := self parseName.
    self expectSpace.
    self expect:$(.
    src := String streamContents:[:out|
        [ stream peek == $) ] whileFalse:[
            out nextPut:stream next
        ].
        stream next.
    ].

    ^Array with: dst with: src

    "Created: / 05-12-2012 / 18:38:19 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePathCopyList
    | list |

    stream atEnd ifTrue:[ ^#() ].
    stream peek isSeparator ifTrue:[ ^#() ].
    list := OrderedCollection new.
    list add: self parsePathCopy.
    [ stream atEnd not and:[stream peek ~= Character cr]] whileTrue:[
        "/stream next. "/eat space.
        list add: self parsePathCopy.
    ].
    ^list.

    "Created: / 05-12-2012 / 18:39:02 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 10-01-2013 / 23:25:10 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePathList
    | list |

    stream atEnd ifTrue:[ ^#() ].
    stream peek isSeparator ifTrue:[ ^#() ].
    list := OrderedCollection new.
    list add: self parsePath.
    [ stream atEnd not and:[stream peek == Character space]] whileTrue:[
        stream next. "/eat space.
        list add: self parsePath.
    ].
    ^list.

    "Created: / 05-12-2012 / 18:27:49 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePushPull
    | info |
    info := HGPushPullInfo new.
    [ stream atEnd ] whileFalse:[
        | ln |
        ln := stream nextLine.
        (ln startsWith:'(run ''hg') ifFalse:[
            (ln startsWith:'remote: ') ifTrue:[
                ln := ln copyFrom: 9
            ].
            self notify: ln.
            (ln startsWith:'added ') ifTrue:[
                self parsePushPullSummaryInto: info from: ln readStream.  
            ]
        ]
    ].
    ^info.
    

"/    #(
"/        'adding changesets'
"/        'adding manifests'
"/        'adding file changes'
"/    ) do:[:info|
"/        stream peek == $r ifTrue:[
"/            self expect: 'remote: '.
"/        ].
"/        self expect:info; expectLineEnd.
"/        self notify:info.
"/    ].
"/    ^self parsePushPullSummary

    "Created: / 04-02-2013 / 15:01:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 30-12-2017 / 08:46:00 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePushPullSummary
    ^self parsePushPullSummaryInto:HGPushPullInfo new

    "Created: / 04-02-2013 / 15:02:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePushPullSummaryInto:info 
    "Example:

        'added 1 changesets with 1 changes to 1 files (+1 heads)'"
    
    | c |

    stream peek == $r ifTrue:[
        self expect:'remote: '.
    ].
    self expect:'added '.
    info setNumChangesets:self parseInteger.
    self expect:' changesets with '.
    info setNumChanges:self parseInteger.
    self expect:' changes to '.
    info setNumFiles:self parseInteger.
    self expect:' files'.
    c := stream next.
    c == Character space ifTrue:[
        self expect:$(.
        c := stream next.
        ('+-' includes:c) ifFalse:[
            self error:('got ''%1'', ''+'' or ''-'' expected' bindWith:c).
        ].
        info 
            setNumHeads:(self parseInteger * ((c == $-) ifTrue:[ -1 ] ifFalse:[ 1 ])).
        ^ info
    ].
    (stream atEnd or:[c == Character cr]) ifTrue:[
        ^ info
    ].
    self error:('got ''%1'', new line or space expected' bindWith:c).

    "Created: / 04-02-2013 / 15:26:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 13-07-2013 / 11:55:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parsePushPullSummaryInto: info from: auxStream

    | oldStream |

    oldStream := stream.
    [
        stream := auxStream.
        self parsePushPullSummaryInto: info
    ] ensure:[
        stream := oldStream.
    ].
    ^info

    "Created: / 13-07-2013 / 11:43:38 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - commands'!

parseCommandBookmarks
    "Parse output of 'hg bookmarks' command. Return collection
     of orphaned HGBookmark"

    ^self parseBookmarks

    "Created: / 20-03-2014 / 01:51:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandBranches
    "Parse output of 'hg branches' command. Return collection
     of orphaned HGBranch"
    
    ^ self parseBranches

    "Created: / 09-03-2019 / 08:19:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandBranchesOld
    "Parse output of 'hg branches' command. Return collection
     of orphaned HGBranch"
    
    ^ self parseBranchesOld

    "Created: / 27-11-2012 / 19:16:12 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 27-11-2012 / 20:21:15 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandHeads
    "Parse output of 'hg heads' command, assuming the template given
     was HGCommandParser templateHeads. Return a list of HGChangesetId."

    | ids |

    ids := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        ids add: self parseNodeId. self expectLineEnd.
    ].
    ^ids

    "Created: / 27-11-2012 / 21:24:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandHelp 
    "Parse output of 'hg help topic'"

    ^ String streamContents:[ :out |
        [ stream atEnd ] whileFalse:[ 
            out nextPutLine: stream nextLine              
        ].
    ]

    "Created: / 07-02-2014 / 10:17:03 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandLocate
    "Filenames are 0-byte separated. Yeah, Mercurial is easy
     to parse"

    | filenames |
    filenames := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        | filename |

        filename := stream nextLine.
        "/ Workaround for Mercurial 2.3.x which includes trailing new line
        (filename size ~~ 1 or:[filename first ~~ Character cr]) ifTrue:[
            filenames add:  filename
        ]
    ].
    ^filenames.

    "Created: / 16-11-2012 / 22:35:14 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 16-12-2012 / 00:09:41 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandLog
    "Parse output of 'hg log' command, assuming the template given
     was HGCommandParser templateLog. Return a list of HGChangeset."

    "/ As of mercurial 3.1.1, a message like
    "/ 'invalid branchheads cache (visible): tip differs
    "/ may be written to the stdout. Following code test for it
    stream peek == $i ifTrue:[ 
        self notify: stream nextLine.  
    ].
    ^self parseLog

    "Created: / 13-11-2012 / 09:09:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 08-10-2014 / 21:22:25 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandLogChildren
    "Parse output of 'hg log <path>' command, assuming the template given
     was HGCommandParser templateLogChildren. Return a list of pairs (HGChangesetId, list of HGChangesetId)"

    | revsAndChildren |

    revsAndChildren := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        | rev children |
        rev := self parseNodeId. self expectLineEnd.
        stream atEnd ifFalse:[
            children := self parseNodeIdList. self expectLineEnd.
        ] ifTrue: [
            children := #().
        ].
        revsAndChildren add: (Array with: rev with: children).
    ].
    ^revsAndChildren

    "Created: / 05-12-2012 / 23:44:07 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandLogIdsOnly
    "Parse output of 'hg log <path>' command, assuming the template given
     was HGCommandParser templateLogFile. Return a list of HGChangesetId."
    
    | ids |

    ids := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        ids add:self parseNodeId.
        self expectLineEnd.
    ].
    ^ ids

    "Created: / 05-12-2012 / 19:15:30 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandMerge
    "Parse output of 'hg update' command. "

    | info c |

    info := HGMergeInfo new.
    [ stream peek isDigit ] whileFalse:[
        [ c := stream peek. c isSeparator ] whileTrue:[ stream next ].
        c == $m ifTrue:[
            self parseMergePath: info.
        ] ifFalse:[
            c == $r ifTrue:[
                self parseMergeRemoteChanged: info
            ] ifFalse:[
                c == $l ifTrue:[
                    self parseMergeLocalChanged: info
                ] ifFalse:[
                    self error:'Unexpected merge line'
                ]
            ]
        ]
    ].
    self parseMergeSummary: info.
    c := stream next.
    c == $( ifTrue:[
        self expect: 'branch merge, don''t forget to commit)'.
        self expectLineEnd.
        ^info
    ].
    c == $u ifTrue:[
        "/ Mercurial 4.6 and newer prints
        "/ 
        "/     use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
        "/ 
        "/ while older versions print
        "/ 
        "/      use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
        "/ 
        "/ We have to support both...

        self expect: 'se ''hg resolve'' to retry unresolved file merges or ''hg '.
        self nextLine. "/ eat the rest of the line
        ^info
    ].
    self error:('Unexpected character ''%1'' expecting ''('' or ''u''' bindWith: c)

    "Created: / 14-01-2013 / 15:57:57 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 23-08-2018 / 10:28:50 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandPull
    "Parse output of 'hg pull' command."

    "
    Ex:

    pulling from ssh://dialin.exept.de/repositories/hg/exept.workflow
    searching for changes
    adding changesets
    adding manifests
    adding file changes
    added 16 changesets with 16 changes to 14 files (+1 heads)
    (run ''hg heads'' to see heads)       

    "
    | c |

    stream atEnd ifTrue:[
        ^ nil
    ].
    
    self expect:'pulling from'. stream nextLine.
    c := stream peek.
    c == $s ifTrue:[
        self expect: 'searching for changes'. stream nextLine.
        self notify: 'searching for changes'.
        c := stream peek.
    ].
    c == $r ifTrue:[
        self expect: 'requesting all changes'. stream nextLine.
        self notify: 'requesting all changes'.
        ^self parsePushPull
    ].
    c == $n ifTrue:[
        self expect: 'no changes found'. stream nextLine.
        self notify: 'no changes found'.
        ^HGPushPullInfo new
    ].

    [ c == $a ] whileTrue:[
        self expect: 'add'.
        c := stream peek.
        c == $i ifTrue:[
            "/ adding ...
            self expect: 'ing '.
            c := stream peek.
            c == $c ifTrue:[
                self expect: 'changesets'. stream nextLine.
                self notify: 'adding changesets'.
            ] ifFalse:[
            c == $m ifTrue:[
                self expect: 'manifests'. stream nextLine.
                self notify: 'adding manifests'.
            ] ifFalse:[
            c == $f ifTrue:[
                self expect: 'file changes'. stream nextLine.
                self notify: 'adding file changes'.  
            ]]]
        ] ifFalse:[
        c == $e ifTrue:[
            "/ added ... ('add' already eaten...)
            | line |

            line := 'add' , stream nextLine.
            ^ self parsePushPullSummaryInto: HGPushPullInfo new from: line readStream.
        ]].
        c := stream peek.
    ].

    self error:('Unexpected character ''%1'' expecting ''r'' or ''n''' bindWith: c)

    "Created: / 04-02-2013 / 15:35:10 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 14-11-2013 / 13:25:20 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandPush
    "Parse output of 'hg push' command. "
    "
     Ex:

     pushing to /tmp/stx_tmp/stxtmp_7733_20/upstream
     searching for changes
    "
    | c |

    stream atEnd ifTrue:[
        ^ nil
    ].

    
    self expect:'pushing to'.stream nextLine.
    stream atEnd ifTrue:[ ^ nil ].
    c := stream peek.
    c == $s ifTrue:[
        self expect: 'searching for changes'. stream nextLine.
        self notify: 'searching for changes'.
        c := stream peek.
    ].
    c == $n ifTrue:[
        self expect:'no changes found'. stream nextLine.
        self notify: 'no changes found'.
        ^ HGPushPullInfo new
    ] ifFalse:[
        ^ self parsePushPull
    ].
    self error:('Unexpected character ''%1'' expecting ''s'' or ''n''' bindWith: c)

    "Created: / 10-12-2012 / 02:15:53 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 12-02-2013 / 23:49:37 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandResolveList
    "Parse output of 'hg resolve --list' command. Return dictionary <path,status>"

    | statuses |

    statuses := Dictionary new.

    [ stream atEnd ] whileFalse:[
        | status path |

        status := stream next.
        (status == $U or:[status == $R]) ifFalse:[
            self error:'Unknown resolution status: ', status.
        ].
        self expectSpace.
        path := self parsePath.
        statuses at: path put: status.
        self expectLineEnd.
    ].
    ^statuses

    "Created: / 14-01-2013 / 16:45:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandShowConfig
    "Parse output of 'hg showconfig' command, assuming the template given
     was HGCommandParser templateLog. Return a list of HGChangeset."

    ^self parseConfig

    "Created: / 06-12-2012 / 16:00:38 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandStatus
    | statusesAndPaths |
    statusesAndPaths := OrderedCollection new.
    [ stream atEnd ] whileFalse:[
        | status path |

        stream peek == Character space ifTrue:[
            | last |

            last := statusesAndPaths last first.
            (last isAdded or:[last isModified]) ifTrue:[
                stream next.
                self expectSpace.
                path := self nextLine.
                statusesAndPaths last at:1 put: (HGStatus copied source: path)
            ] ifFalse:[
                self error:'Malformed status output, status code expected, got space'
            ]
        ] ifFalse:[
            status := HGStatus forCode: self next.
            self expectSpace.
            path := self nextLine.
            statusesAndPaths add: { status . path }
        ].
    ].
    ^ statusesAndPaths

    "Created: / 23-10-2012 / 10:57:06 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 10-01-2019 / 21:04:36 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandUpdate
    "Parse output of 'hg update' command. "

    ^self parseMergeSummary

    "Created: / 14-01-2013 / 15:48:14 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseCommandVersion
    "Parse output of 'hg --version'"

    "
    Mercurial Distributed SCM (version 2.3.2)
    (see http://mercurial.selenic.com for more information)
    
    Copyright (C) 2005-2012 Matt Mackall and others
    This is free software; see the source for copying conditions. There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    "

    | major minor revision |

    self 
        expect:'Mercurial'; skipSeparators;
        expect:'Distributed'; skipSeparators;
        expect:'SCM'; skipSeparators;
        expect:$(; skipSeparators;
        expect:'version'.

    major := self parseInteger.
    self expect:$..
    minor := self parseInteger.
    stream peek == $. ifTrue:[
        stream next.
        revision := self parseInteger.
    ].

    (stream peek ~~ $) and:[ stream peek ~~ $+]) ifTrue:[ 
        self error: ('Expected ''$)'' or ''+'' got ''%2''.' bindWith: stream peek).
    ].

    ^(Array with: major with: minor with: revision)

    "Created: / 19-11-2012 / 20:19:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 01-12-2014 / 20:25:48 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - errors'!

parseError
    ^self parseErrorClass: HGCommandError

    "Created: / 04-02-2013 / 12:21:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseError: parseBlock
    [ stream atEnd ] whileFalse:[
        self parseError1: parseBlock.  
    ].
    ^nil

    "Created: / 04-02-2013 / 12:21:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-11-2014 / 00:29:22 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseErrorBookmark
    ^self parseErrorClass: HGBookmarkError

    "Created: / 20-03-2014 / 17:28:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseErrorBranches
    [ stream atEnd ] whileFalse:[
        | c word |

        c := stream peek.
        c == $i ifTrue:[
            "/ Mercurial <  2.7 uses 'invalidating branch cache (tip differs)'.
            "/ Mercurial >= 2.7 uses 'invalid branchheads cache (served): tip differs'
            "/                    or 'invalid branchheads cache (visible): tip differs'
            "/ Sigh...
            self expect: 'invalid'.
            c := stream peek.
            c == $a ifTrue:[
                self expect: 'ating branch cache'.
                stream nextLine. "/ eat reast of the line
            ] ifFalse:[c == Character space ifTrue:[
                self expect: ' branchheads cache'.
                stream nextLine. "/ eat reast of the line
            ]].


        ] ifFalse:[
            self parseError1: [ :msg | self error: msg ]  
        ]
    ].
    ^nil

    "Created: / 06-02-2013 / 19:18:58 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 06-11-2014 / 00:43:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseErrorClass: errorClass
    "Generic error output parse. Returns an initialized
     error (instance of errorClass) if an error occures,
     nil if not.

     An error is indicated by 'abort: ' prefix."

    self parseError:[:msg|
        self propagate: errorClass message: msg
    ].

    "Created: / 04-02-2013 / 12:50:03 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 04-02-2013 / 21:58:25 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseErrorCommit
    ^self parseErrorClass: HGCommitError

    "Created: / 04-02-2013 / 12:21:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseErrorPush
    ^self parseError: [:msg |
        (msg startsWith: 'push creates new remote head ') ifTrue:[
            | newHeadId err |

            newHeadId := HGChangesetId fromString: (msg copyFrom: 30 to: msg size -1).
            err := HGPushWouldCreateNewHeadError newException
                        parameter: newHeadId;
                        messageText: msg;
                        yourself.
            self propagate: err.                          
        ] ifFalse:[
            self propagate: HGError message: msg
        ].

    ].

    "Created: / 04-02-2013 / 12:49:45 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 26-03-2014 / 15:39:57 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - errors - private'!

parseError1: parseBlock
    "/ Parse at most one error from stream and returs.

    | c word line |

    c := stream peek.
    c isNil ifTrue:[ ^ self ]. "/ stream closed / already at end

    "/ Care for "*** failed to import extension" messages...
    c == $* ifTrue:[ 
        stream next.
        c := stream peek.
        c == $* ifTrue:[ 
            stream next.
            c := stream peek.
            c == $* ifTrue:[ 
                stream next.
                self notify: stream nextLine.
                ^ self.
            ].
        ].
        self notify: stream nextLine.
        ^ self.
    ].
    "/ parse `abort: some error messsage` error format
    c == $a ifTrue:[
        word := stream upTo: $:.
        stream next. "/eat space
        word = 'abort' ifTrue:[
            self parseError1: parseBlock message: stream nextLine.              
            ^ self.
        ].
    ].
    "/ parse `hg: parse error: some error messsage` error format introduced in Mercurial 4.3
    c == $h ifTrue:[
        word := stream upTo: $:.
        word = 'hg' ifTrue:[
            stream next. "/eat space
            c := stream peek.
            c == $p ifTrue:[
                self expect: 'parse error: '.
                self parseError1: parseBlock message: stream nextLine.                
                ^ self.
            ].
        ].
    ].


    "/ Special hack for mercurial_keyring extension, sigh...
    line := stream nextLine.
    "/ If c == $a we may have already read some data. In that case,
    "/ word is not nil and we have to preprend it to line read just above...
    word notNil ifTrue:[ 
        line := word , (line ? '')
    ].

    (line includesSubString: 'mercurial_keyring.py') ifTrue:[
        (line endsWith: 'UserWarning: Basic Auth Realm was unquoted') ifTrue:[
            stream nextLine.
        ].
        ^ self.
    ].
    self notify: 'Unexpected error output: ', line.
    ^ self.

    "Created: / 06-11-2014 / 00:28:44 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 17-10-2017 / 09:55:50 / jv"
    "Modified: / 08-02-2018 / 08:51:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseError1: parseBlock message: errorMessage
    "Parse given `errorMmessage` using `parseBlock`. Handle common errors here,
     pass the rest to the parseBlock to handle."

    (errorMessage startsWith: 'unknown revision ''') ifTrue:[
        | rev err |

        rev := HGChangesetId readFrom: (errorMessage readStream skip: 'unknown revision ''' size) onError:[ nil ].
        err := HGUnknownRevisionError newException
                    parameter: rev;
                    messageText: errorMessage; yourself.
        self propagate: err.
        ^ self.
    ].
    (errorMessage startsWith: 'hidden revision ''') ifTrue:[
        | rev err |

        rev := HGChangesetId readFrom: (errorMessage readStream skip: 'hidden revision ''' size) onError:[ self error: 'Cannot parse changeset ID from error message!!' ].
        err := HGObsoleteRevisionError newException
                    parameter: rev;
                    messageText: errorMessage; yourself.
        self propagate: err.
        ^ self.
    ].
    parseBlock value: errorMessage

    "Created: / 08-02-2018 / 08:51:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - files'!

parseDotHgBookmarks
    "Parse contents of .hg/bookmarks, return a collection
     of orphaned HGBookmark"

    | bookmarks |

    bookmarks := OrderedCollection new.
    [ stream atEnd ] whileFalse:[ 
        | bookmark |

        bookmark := HGBookmark new.
        bookmark setChangesetId: self parseNodeId.
        stream skipSeparators.
        bookmark setName: stream nextLine.
        bookmarks add: bookmark
    ].
    ^ bookmarks

    "Created: / 20-03-2014 / 02:10:54 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 20-03-2014 / 18:53:00 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - shell'!

parseShellCommand
    "Parses input stream as a shell command. Returns
     an array of arguments (main's argv[], actually)"

    OperatingSystem isMSWINDOWSNTlike ifTrue:[ 
        ^ self parseShellCommandAsForCmd
    ] ifFalse:[
        ^ self parseShellCommandAsForSh
    ]

    "Created: / 17-07-2014 / 12:25:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseShellCommandAsForCmd
    ^ Array streamContents:[ :argv | 
        stream skipSeparators.
        [ stream atEnd ] whileFalse:[
            argv nextPut: self parseShellCommandTokenAsForCmd.
            stream skipSeparators.   
        ].
    ].

    "Created: / 17-07-2014 / 12:53:32 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseShellCommandAsForSh
    ^ Array streamContents:[ :argv | 
        stream skipSeparators.
        [ stream atEnd ] whileFalse:[
            argv nextPut: self parseShellCommandTokenAsForSh.
            stream skipSeparators.   
        ].
    ].

    "Created: / 17-07-2014 / 12:53:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseShellCommandTokenAsForCmd
    | buffer char done1 done2 |

    buffer := (String new: 10) writeStream.
    done1 := false.
    char := stream next.
    [ done1 ] whileFalse:[
        char == $" ifTrue:[ 
            done2 := false.
            [ done2 ] whileFalse:[
                stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                char := stream next.
                char == $" ifTrue:[
                    done2 := true.
                ] ifFalse:[ 
                    buffer nextPut: char      
                ].
            ]
        ] ifFalse:[ 
            char == $^ ifTrue:[ 
                stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                char := stream next.                    
                char == Character space ifTrue:[ 
                    buffer nextPut: $^ 
                ].
            ].
            buffer nextPut: char.   
        ].
        char := stream atEnd ifTrue:[ nil ] ifFalse:[ stream next ].  
        done1 := char isNil or:[ char isSeparator ].
    ].
    ^ buffer contents

    "Created: / 17-07-2014 / 12:32:42 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 17-07-2014 / 14:31:47 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

parseShellCommandTokenAsForSh
    | buffer char done1 done2 |

    buffer := (String new: 10) writeStream.
    done1 := false.
    char := stream next.
    [ done1 ] whileFalse:[
        char == $" ifTrue:[ 
            done2 := false.
            [ done2 ] whileFalse:[
                stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                char := stream next.
                char == $\ ifTrue:[ 
                    stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                    char := stream next.
                    char == $" ifTrue:[ 
                        buffer nextPut: $"  
                    ] ifFalse:[ 
                        buffer nextPut: $\; nextPut: char  
                    ].
                ] ifFalse:[ char == $" ifTrue:[
                    done2 := true.
                ] ifFalse:[ 
                    buffer nextPut: char      
                ]].
            ]
        ] ifFalse:[ char == $' ifTrue:[ 
            done2 := false.
            [ done2 ] whileFalse:[
                stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                char := stream next.
                char == $' ifTrue:[
                    done2 := true.
                ] ifFalse:[ 
                    buffer nextPut: char      
                ].                       
            ]
        ] ifFalse:[ 
            char == $\ ifTrue:[ 
                stream atEnd ifTrue:[ self error:'Unterminated string token'. ^ nil ].
                char := stream next.                    
            ].
            buffer nextPut: char.   
        ]].
        char := stream atEnd ifTrue:[ nil ] ifFalse:[ stream next ].  
        done1 := char isNil or:[ char isSeparator ].
    ].
    ^ buffer contents

    "Created: / 17-07-2014 / 12:32:42 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 17-07-2014 / 14:11:27 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser methodsFor:'parsing - utils'!

expect: aStringOrChar

    | c |
    aStringOrChar isCharacter ifTrue:[
        (stream atEnd or:[(c := stream next) ~= aStringOrChar]) ifTrue:[
            self error:('Expected ''%1'' got ''%2''.' bindWith: aStringOrChar with: c).
        ].
        ^self.
    ].
    aStringOrChar isString ifTrue:[
        aStringOrChar do:[:expected|
            (stream atEnd or:[(c := stream next) ~= expected]) ifTrue:[
                self error:('Expected ''%1''.' bindWith: aStringOrChar).
            ].
        ].
        ^self.
    ].

    self error:'Invalid expected value'.

    "Created: / 19-11-2012 / 20:08:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

expectLineEnd
    self expect: Character cr.

    "Created: / 19-11-2012 / 20:06:21 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

expectSpace
    self expect: Character space.

    "Created: / 19-11-2012 / 20:06:27 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

next
    ^stream next.

    "Created: / 23-10-2012 / 10:57:24 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

nextLine
    ^stream nextLine

    "Created: / 23-10-2012 / 11:05:47 / Jan Vrany <jan.vrany@fit.cvut.cz>"
    "Modified: / 09-11-2012 / 12:02:08 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

skipSeparators
    stream skipSeparators

    "Created: / 19-11-2012 / 20:05:04 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!HGCommandParser class methodsFor:'documentation'!

version_HG

    ^ '$Changeset: <not expanded> $'
!

version_SVN
    ^ '$Id$'
! !