[PATCH 2 of 3 RFC -V3] Implementation of basic hg commands via low level API (stories)

Giovanni Gherdovich g.gherdovich at gmail.com
Mon Aug 5 00:03:51 CDT 2013


Hello Iulian,

from your code it looks like you forgot that after you feed some
input into the 'L' channel, you have to terminate your input with
a zero-length message.

Look here: http://mercurial.selenic.com/wiki/CommandServer

"length = 0 sent by the client is interpreted as EOF by the server."

My comments interleaved in your code.

:::: # HG changeset patch
:::: # User Iulian Stana <julian.stana at gmail.com>
:::: # Date 1374753623 -10800
:::: #      Thu Jul 25 15:00:23 2013 +0300
:::: # Node ID a97753ae4036ba4ea34ccb67971d31c01f97ed4b
:::: # Parent  2cc0845928f01bf161cb1d4983bd029c7c3e70aa
:::: Implementation of basic hg commands via low level API (stories)
::::
:::: The main.c file is not “real code”, just a showcase of examples. It is
more of a
:::: documentation than actual code. It contains the "stories" that Matt
asked me to
:::: prepare. Here I created some scenarios to test those stories.
::::
:::: Those examples stands with purpose to show that you can implement and
handle in
:::: some ways all the mercurial commands, using just the level 0 of
c-hglib API.
::::
:::: a) The init command, which starts with no repository. For this story I
use the
:::: following approach: first I create a new process where I will execute
the init
:::: command with plain hg, and after the child ends I establish the
connection
:::: with the new repository.
::::
:::: b) The log command, which can produce huge data. The data is read in
chunks. In
:::: my example I just print this data to the standard output, but the user
is free
:::: to do what ever he wants with this data. (write it into a file or
process it)
::::
:::: c) The import command, which can deal with the command server input
channels. On
:::: this task, I was able to show that you can import a patch using stdin.
In the
:::: same time you can specify a file to simulate the stdin process.
::::
:::: d) The merge command, which can deal prompts. Sometimes when a
conflict appears,
:::: the merge command could use those prompts to interact with user
desires.
:::: In my function I gave an example on how a user could pass a prompt
procedure, a
:::: procedure where the interation is handled. (in my procedure function
the answer
:::: will be received from stdin)
::::
:::: e) The verify command, where the command server could send output and
error
:::: data. The received data will be split in two. You will know the type
of data
:::: (output data or error data)  with the channel help, who can be found
in the
:::: header structure of the handle.
::::
:::: f) Export-import without saving data into ram or disk memory. There
will be two
:::: connections, one for the export repository (export handle) and one for
the
:::: import repository ( import handle). Immediately after you receive any
kind of
:::: data from the export handle, you will pass it to the import handle.
::::
:::: g) Add 'filename with spaces' example.
::::
:::: All of those examples are build with level 0 function witout using
fancy
:::: functions.
:::: I refer to those implementation as 'by hand' ones, as opposed to
'using a lot of
:::: technology and abstraction.
::::
:::: diff --git a/main.c b/main.c
:::: new file mode 100644
:::: --- /dev/null
:::: +++ b/main.c
:::: @@ -0,0 +1,444 @@
:::: +#include <stdio.h>
:::: +#include <stdlib.h>
:::: +#include <string.h>
:::: +
:::: +#include <sys/wait.h>
:::: +#include <sys/types.h>
:::: +#include <unistd.h>
:::: +#include <sys/stat.h>
:::: +#include <fcntl.h>
:::: +
:::: +#include "client.h"
:::: +#include "utils.h"
:::: +
:::: +#define INIT_REPO  "init_test_repo"
:::: +
:::: +/****** Convenience functions. *******/
:::: +
:::: +/* Setup the tmp directory where the magic happends.*/
:::: +void setup_tmp()
:::: +{
:::: +    system("hg init tmp");
:::: +    chdir("tmp");
:::: +}
:::: +
:::: +void clean_tmp()
:::: +{
:::: +    chdir("..");
:::: +    system("rm -rf tmp");
:::: +}
:::: +
:::: +/* Setup for init command. */
:::: +void setup_init()
:::: +{
:::: +    system("mkdir tmp");
:::: +    chdir("tmp");
:::: +}
:::: +
:::: +/* Create some commits in the created repo. */
:::: +void post_init_setup()
:::: +{
:::: +    chdir(INIT_REPO);
:::: +    system("touch foo ; hg add foo ; hg commit -m foo");
:::: +    system("echo baloo > foo ; hg commit -m 'baloo text'");
:::: +    chdir("..");
:::: +}
:::: +
:::: +/* Setup for log command. */
:::: +void setup_log()
:::: +{
:::: +    system("touch foo ; hg add foo ; hg commit -m foo");
:::: +    system("echo baloo > foo ; hg commit -m 'baloo text'");
:::: +    system("touch voodoo ; hg add voodoo ; hg commit -m voodoo");
:::: +    system("echo voodoo > voodoo ; hg commit -m 'voodoo text'");
:::: +}
:::: +
:::: +/* Setup the merge command. Without conflicts.*/
:::: +void setup_merge()
:::: +{
:::: +    system("touch foo ; hg add foo ; hg commit -m foo");
:::: +    system("echo baloo > foo ; hg commit -m 'baloo text'");
:::: +    system("hg up 0");
:::: +    system("touch boo ; hg add boo ; hg commit -m boo");
:::: +    system("echo voodoo > boo ; hg commit -m 'voodoo text'");
:::: +}
:::: +
:::: +/* Setup the merge command. Create a conflict that will have
prompts.*/
:::: +void setup_conflict_merge()
:::: +{
:::: +    system("touch foo ; hg add foo ; hg commit -m foo");
:::: +    system("echo baloo > foo ; hg commit -m 'baloo text'");
:::: +    system("hg up 0");
:::: +    system("echo voodoo > foo ; hg commit -m 'voodoo text'");
:::: +}
:::: +
:::: +/* Setup for export_import process.*/
:::: +void setup_export_import()
:::: +{
:::: +    system("hg init export");
:::: +    chdir("export");
:::: +    system("touch foo ; echo baloo > foo; hg add foo ; hg commit -m
foo");
:::: +    chdir("..");
:::: +    system("hg init import");
:::: +}
:::: +
:::: +void clean_export_import()
:::: +{
:::: +    system("rm -rf export import");
:::: +}
:::: +
:::: +/* Setup for filename_with_space example.*/
:::: +void setup_filename_with_space()
:::: +{
:::: +    system("touch 'foo bar'");
:::: +    printf("---- ls command ----\n");
:::: +    system("ls -l");
:::: +}
:::: +
:::: +/******* By hand implementations. ******/
:::: +
:::: +/*
:::: + * Create a repo to the specific path, and then will open the
connection with
:::: + * this new repo.
:::: + * The clone command will follow the same steps. Clone repo; open
connection.
:::: + * */
:::: +hg_handle *hg_init_by_hand()
:::: +{
:::: +    pid_t cpid;
:::: +    int status;
:::: +    hg_handle *handle;
:::: +    char command[50];
:::: +
:::: +    sprintf(command, "hg init %s", INIT_REPO);
:::: +
:::: +    if((cpid = fork()) < 0) {
:::: +        printf("Fork failed\n");
:::: +        return NULL;
:::: +
:::: +    } else if(cpid == 0) {
:::: +        execl("/bin/sh", "sh", "-c", command, NULL);
:::: +        printf("dadads\n\n");
:::: +    } else {
:::: +         waitpid( cpid, &status, 0);
:::: +    }
:::: +
:::: +    handle = hg_open(INIT_REPO, "");
:::: +
:::: +    return handle;
:::: +}
:::: +
:::: +/*
:::: + * Will read a huge mass of data in chunks and will print this data
on stdout.
:::: + * */
:::: +int hg_log_by_hand(hg_handle *handle)
:::: +{
:::: +    char buff[4096];
:::: +    char *comm[] = {"log", "-v"};
:::: +    int exitcode;
:::: +    int ns;
:::: +
:::: +    hg_rawcommand(handle, comm, 2);
:::: +
:::: +    while(hg_channel(handle) != 'r'){
:::: +        while(ns = hg_rawread(handle, buff, 4096), ns > 0){
:::: +            printf("%s", buff);
:::: +        }
:::: +    }
:::: +
:::: +    exitcode = hg_exitcode(handle);
:::: +    printf("exitcode = %d\n", exitcode);
:::: +
:::: +    return exitcode;
:::: +}
:::: +
:::: +/*
:::: + * The purpose is to show the functionality, it's not the import
function.
:::: + * This function will import to the handle the import_patch, which is
a file or
:::: + * will use the stdin if the import_patch is NULL.
:::: + * */
:::: +int hg_import_by_hand(hg_handle *handle, char *import_patch)
:::: +{
:::: +    char *comm[] = {"import", "-"};
:::: +    char buff[4096];
:::: +    int exitcode = 0;
:::: +    int fd, ns;
:::: +    hg_header header;
:::: +
:::: +    fd = open(import_patch, O_RDONLY);
:::: +    hg_rawcommand(handle, comm, 2);
:::: +
:::: +    while(hg_channel(handle) != 'r'){
:::: +        /* outchannels 'o' or 'e'. */
:::: +        while(ns = hg_rawread(handle, buff, 4096), ns > 0){
:::: +            printf("%s", buff);
:::: +        }
:::: +        if(hg_channel(handle) == 'L'){
:::: +            header = hg_head(handle);
:::: +            int length = read(fd, buff, header.length);
:::: +            hg_rawwrite(handle, buff, length);
:::: +        }
:::: +    }

The while loop above, inside which you manage input to the 'L' channel,
works for a subtle reason, and I am not sure it was what you intended:
after you're done feeding the patch into the command server, this latter
will ask for more input via prompting you with a ('L', 4096) line.

So, your code will enter the 'if' branch once again; but the file
with the patch has all been consumed, and the read in

int length = read(fd, buff, header.length);

will return 0. Then you feed this zero-length message into

hg_rawwrite(handle, buff, length);

which terminates the 'L' channel input session. Not bad,
only you didn't realize that I think :)

:::: +
:::: +    exitcode = hg_exitcode(handle);
:::: +    return exitcode;
:::: +}
:::: +
:::: +/* The prompt function for the merge funtion.*/
:::: +char prompt_function(char *output)
:::: +{
:::: +    char option;
:::: +    scanf("\n%c", &option);
:::: +    return option;
:::: +}
:::: +
:::: +/*
:::: + * The merge function gets a prompt function that deals with promps.
:::: + * It's just a default function, to show the mechanism(is't not a
level 0 issue)
:::: + * This function show that you can create a merge function that
handle prompts
:::: + * with level 0 API.
:::: + * */
:::: +int hg_merge_by_hand(hg_handle *handle, char (*prompt)(char *))
:::: +{
:::: +    char *comm[] = {"merge", "--tool=internal:prompt"};
:::: +    char buff[4096];
:::: +    int exitcode = 0;
:::: +    int ns;
:::: +
:::: +    hg_rawcommand(handle, comm, 2);
:::: +
:::: +    while(hg_channel(handle) != 'r'){
:::: +        /* outchannels 'o' or 'e'. */
:::: +        while(ns = hg_rawread(handle, buff, 4096), ns > 0){
:::: +            printf("%s", buff);
:::: +        }
:::: +        if(hg_channel(handle) == 'I'){
:::: +            printf("INPUT\n");
:::: +        }
:::: +        else if(hg_channel(handle) == 'L'){
:::: +            /* In stand of the default prompt_msg, the argument for
:::: +             * prompt function will be the output data received
:::: +             * until this moment.
:::: +             * */
:::: +            char option = (*prompt)(NULL);
:::: +            printf("\noption = %c\n", option);
:::: +            hg_rawwrite(handle, &option, 1);
:::: +            /* TODO: is a small issue, but not for this level.
:::: +             * The second write, must not be necessary.
:::: +             * */
:::: +            hg_rawwrite(handle, &option, 0);
:::: +        }
:::: +    }

Here you do some voodoo programming
http://www.catb.org/jargon/html/V/voodoo-programming.html
by adding the line

hg_rawwrite(handle, &option, 0);

why is that needed? Because, again, after you send stuff into ('L', 4096)
you need to tell command server that you're done via a zero length message.
Which is,

hg_rawwrite(handle, '', 0);

would have also worked.

If you don't have already something like this, here is what I use
to have interactive conversation with the command server:

first off, a couple of function that I put in a file named util.py:

# -- -- -- -- -- -- -- -- util.py -- -- -- --
import sys, struct, subprocess

class server:

    def __init__(self):
        self.server = subprocess.Popen(['hg',
                                        '--config',
                                        'ui.interactive=True',
                                        'serve',
                                        '--cmdserver',
                                        'pipe'],
                                       stdin=subprocess.PIPE,
                                       stdout=subprocess.PIPE)

    def readchannel(self):
        channel, length = struct.unpack('>cI',
                                        self.server.stdout.read(5))
        if channel in 'IL': # input
            return channel, length
        return channel, length, self.server.stdout.read(length)

    def writeblock(self, data):
        self.server.stdin.write(struct.pack('>I', len(data)))
        self.server.stdin.write(data)
        self.server.stdin.flush()
# -- -- -- -- -- -- -- -- end of util.py -- -- -- --

(it is basically the client at
http://mercurial.selenic.com/wiki/CommandServer#Example_client )

Then I run sessions on the python shell like this one:

>>> # -- -- -- -- -- -- -- -- interactive python session -- -- -- --
>>> import sys
>>>
>>> from util import *
>>>
>>> s = server()
>>>
>>> hello = s.readchannel()
>>> hello
('o', 52, 'capabilities: getencoding runcommand\nencoding: UTF-8')
>>>
>>> s.server.stdin.write('runcommand\n')
>>> s.writeblock('\0'.join('import -'.split()))
>>> ans = s.readchannel()
>>> repr(ans)
"('o', 26, 'applying patch from stdin\\n')"
>>> ans = s.readchannel()
>>> repr(ans)
"('L', 4096)"
>>>
>>> patch = """# HG changeset patch
... # User gghh
... # Date 1375612762 -7200
... #      Sun Aug 04 12:39:22 2013 +0200
... # Node ID 644e1273b89bf82704dbe8521243d77bfa4756f2
... # Parent  0000000000000000000000000000000000000000
... foo
...
... diff -r 000000000000 -r 644e1273b89b foo
... --- /dev/null    Thu Jan 01 00:00:00 1970 +0000
... +++ b/foo    Sun Aug 04 12:39:22 2013 +0200
... @@ -0,0 +1,1 @@
... +baloo
... """
>>>
>>> s.writeblock(patch)
>>> ans = s.readchannel()
>>> repr(ans)
"('L', 4096)"
>>>
>>> s.writeblock('')
>>> ans = s.readchannel()
>>> repr(ans)
"('r', 4, '\\x00\\x00\\x00\\x00')"
>>>

you can see that I have to send a empty message with s.writeblock('').

That's it; you will have to consider this when implementing level 1 stuff.

Cheers,
Giovanni
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://selenic.com/pipermail/mercurial-devel/attachments/20130805/31cce16f/attachment.html>


More information about the Mercurial-devel mailing list