View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c) 2010-2013, University of Amsterdam,
    7                             VU University
    8    Amsterdam All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(git,
   37          [ git/2,                      % +Argv, +Options
   38            git_process_output/3,       % +Argv, :OnOutput, +Options
   39            git_open_file/4,            % +Dir, +File, +Branch, -Stream
   40            is_git_directory/1,         % +Dir
   41            git_describe/2,             % -Version, +Options
   42            git_hash/2,                 % -Hash, +Options
   43            git_ls_tree/2,              % -Content, +Options
   44            git_remote_url/3,           % +Remote, -URL, +Options
   45            git_ls_remote/3,            % +GitURL, -Refs, +Options
   46            git_branches/2,             % -Branches, +Options
   47            git_remote_branches/2,      % +GitURL, -Branches
   48            git_default_branch/2,       % -DefaultBranch, +Options
   49            git_tags_on_branch/3,       % +Dir, +Branch, -Tags
   50            git_shortlog/3,             % +Dir, -Shortlog, +Options
   51            git_log_data/3,             % +Field, +Record, -Value
   52            git_show/4,                 % +Dir, +Hash, -Commit, +Options
   53            git_commit_data/3           % +Field, +Record, -Value
   54          ]).   55:- use_module(library(process)).   56:- use_module(library(readutil)).   57:- use_module(library(option)).   58:- use_module(library(dcg/basics)).   59:- use_module(library(record)).   60:- use_module(library(lists)).   61:- use_module(library(error)).   62
   63:- meta_predicate
   64    git_process_output(+, 1, +).   65
   66/** <module> Run GIT commands
   67
   68This module performs common GIT tasks by calling git as a remote process
   69through process_create/3. It requires that the =git= executable is in the
   70current PATH.
   71
   72This module started life in ClioPatria and   has been used by the Prolog
   73web-server to provide information on git   repositories. It is now moved
   74into the core Prolog library to support the Prolog package manager.
   75*/
   76
   77:- predicate_options(git/2, 2,
   78                     [ directory(atom),
   79                       error(-codes),
   80                       output(-codes),
   81                       status(-any),
   82                       askpass(any)
   83                     ]).   84:- predicate_options(git_default_branch/2, 2,
   85                     [ pass_to(git_process_output/3, 3)
   86                     ] ).   87:- predicate_options(git_describe/2, 2,
   88                     [ commit(atom),
   89                       directory(atom),
   90                       match(atom)
   91                     ]).   92:- predicate_options(git_hash/2, 2,
   93                     [ commit(atom),
   94                       directory(atom)
   95                     ]).   96:- predicate_options(git_ls_tree/2, 2,
   97                     [ commit(atom),
   98                       directory(atom)
   99                     ]).  100:- predicate_options(git_process_output/3, 3,
  101                     [ directory(atom),
  102                       askpass(any),
  103                       error(-codes)
  104                     ]).  105:- predicate_options(git_remote_url/3, 3,
  106                     [ pass_to(git_process_output/3, 3)
  107                     ]).  108:- predicate_options(git_shortlog/3, 3,
  109                     [ revisions(atom),
  110                       limit(nonneg),
  111                       path(atom)
  112                     ]).  113:- predicate_options(git_show/4, 4,
  114                     [ diff(oneof([patch,stat]))
  115                     ]).  116
  117
  118%!  git(+Argv, +Options) is det.
  119%
  120%   Run a GIT command.  Defined options:
  121%
  122%     * directory(+Dir)
  123%     Execute in the given directory
  124%     * output(-Out)
  125%     Unify Out with a list of codes representing stdout of the
  126%     command.  Otherwise the output is handed to print_message/2
  127%     with level =informational=.
  128%     * error(-Error)
  129%     As output(Out), but messages are printed at level =error=.
  130%     * askpass(+Program)
  131%     Export GIT_ASKPASS=Program
  132
  133git(Argv, Options) :-
  134    git_cwd_options(Argv, Argv1, Options),
  135    env_options(Extra, Options),
  136    setup_call_cleanup(
  137        process_create(path(git), Argv1,
  138                       [ stdout(pipe(Out)),
  139                         stderr(pipe(Error)),
  140                         process(PID)
  141                       | Extra
  142                       ]),
  143        call_cleanup(
  144            ( read_stream_to_codes(Out, OutCodes, []),
  145              read_stream_to_codes(Error, ErrorCodes, [])
  146            ),
  147            process_wait(PID, Status)),
  148        close_streams([Out,Error])),
  149    print_error(ErrorCodes, Options),
  150    print_output(OutCodes, Options),
  151    (   option(status(Status0), Options)
  152    ->  Status = Status0
  153    ;   Status == exit(0)
  154    ->  true
  155    ;   throw(error(process_error(git(Argv), Status), _))
  156    ).
  157
  158git_cwd_options(Argv0, Argv, Options) :-
  159    option(directory(Dir), Options),
  160    !,
  161    Argv = ['-C', file(Dir) | Argv0 ].
  162git_cwd_options(Argv, Argv, _).
  163
  164env_options([env(['GIT_ASKPASS'=Program])], Options) :-
  165    option(askpass(Exe), Options),
  166    !,
  167    exe_options(ExeOptions),
  168    absolute_file_name(Exe, PlProg, ExeOptions),
  169    prolog_to_os_filename(PlProg, Program).
  170env_options([], _).
  171
  172exe_options(Options) :-
  173    current_prolog_flag(windows, true),
  174    !,
  175    Options = [ extensions(['',exe,com]), access(read) ].
  176exe_options(Options) :-
  177    Options = [ access(execute) ].
  178
  179print_output(OutCodes, Options) :-
  180    option(output(Codes), Options),
  181    !,
  182    Codes = OutCodes.
  183print_output([], _) :- !.
  184print_output(OutCodes, _) :-
  185    print_message(informational, git(output(OutCodes))).
  186
  187print_error(OutCodes, Options) :-
  188    option(error(Codes), Options),
  189    !,
  190    Codes = OutCodes.
  191print_error([], _) :- !.
  192print_error(OutCodes, _) :-
  193    phrase(classify_message(Level), OutCodes, _),
  194    print_message(Level, git(output(OutCodes))).
  195
  196classify_message(error) -->
  197    string(_), "fatal:",
  198    !.
  199classify_message(error) -->
  200    string(_), "error:",
  201    !.
  202classify_message(warning) -->
  203    string(_), "warning:",
  204    !.
  205classify_message(informational) -->
  206    [].
  207
  208%!  close_streams(+Streams:list) is det.
  209%
  210%   Close a list of streams, throwing the first error if some stream
  211%   failed to close.
  212
  213close_streams(List) :-
  214    phrase(close_streams(List), Errors),
  215    (   Errors = [Error|_]
  216    ->  throw(Error)
  217    ;   true
  218    ).
  219
  220close_streams([H|T]) -->
  221    { catch(close(H), E, true) },
  222    (   { var(E) }
  223    ->  []
  224    ;   [E]
  225    ),
  226    close_streams(T).
  227
  228
  229%!  git_process_output(+Argv, :OnOutput, +Options) is det.
  230%
  231%   Run a git-command and process the output with OnOutput, which is
  232%   called as call(OnOutput, Stream).
  233
  234git_process_output(Argv, OnOutput, Options) :-
  235    git_cwd_options(Argv, Argv1, Options),
  236    env_options(Extra, Options),
  237    setup_call_cleanup(
  238        process_create(path(git), Argv1,
  239                       [ stdout(pipe(Out)),
  240                         stderr(pipe(Error)),
  241                         process(PID)
  242                       | Extra
  243                       ]),
  244        call_cleanup(
  245            ( call(OnOutput, Out),
  246              read_stream_to_codes(Error, ErrorCodes, [])
  247            ),
  248            git_wait(PID, Out, Status)),
  249        close_streams([Out,Error])),
  250    print_error(ErrorCodes, Options),
  251    (   Status = exit(0)
  252    ->  true
  253    ;   throw(error(process_error(git, Status)))
  254    ).
  255
  256git_wait(PID, Out, Status) :-
  257    at_end_of_stream(Out),
  258    !,
  259    process_wait(PID, Status).
  260git_wait(PID, Out, Status) :-
  261    setup_call_cleanup(
  262        open_null_stream(Null),
  263        copy_stream_data(Out, Null),
  264        close(Null)),
  265    process_wait(PID, Status).
  266
  267
  268%!  git_open_file(+GitRepoDir, +File, +Branch, -Stream) is det.
  269%
  270%   Open the file File in the given bare GIT repository on the given
  271%   branch (treeisch).
  272%
  273%   @bug    We cannot tell whether opening failed for some reason.
  274
  275git_open_file(Dir, File, Branch, In) :-
  276    atomic_list_concat([Branch, :, File], Ref),
  277    process_create(path(git),
  278                   [ '-C', file(Dir), show, Ref ],
  279                   [ stdout(pipe(In))
  280                   ]),
  281    set_stream(In, file_name(File)).
  282
  283
  284%!  is_git_directory(+Directory) is semidet.
  285%
  286%   True if Directory is a  git   directory  (Either  checked out or
  287%   bare).
  288
  289is_git_directory(Directory) :-
  290    directory_file_path(Directory, '.git', GitDir),
  291    exists_directory(GitDir),
  292    !.
  293is_git_directory(Directory) :-
  294    exists_directory(Directory),
  295    git(['rev-parse', '--git-dir'],
  296        [ output(Codes),
  297          error(_),
  298          status(Status),
  299          directory(Directory)
  300        ]),
  301    Status == exit(0),
  302    string_codes(".\n", Codes).
  303
  304%!  git_describe(-Version, +Options) is semidet.
  305%
  306%   Describe the running version  based  on   GIT  tags  and hashes.
  307%   Options:
  308%
  309%       * match(+Pattern)
  310%       Only use tags that match Pattern (a Unix glob-pattern; e.g.
  311%       =|V*|=)
  312%       * directory(Dir)
  313%       Provide the version-info for a directory that is part of
  314%       a GIT-repository.
  315%       * commit(+Commit)
  316%       Describe Commit rather than =HEAD=
  317%
  318%   @see git describe
  319
  320git_describe(Version, Options) :-
  321    (   option(match(Pattern), Options)
  322    ->  true
  323    ;   git_version_pattern(Pattern)
  324    ),
  325    (   option(commit(Commit), Options)
  326    ->  Extra = [Commit]
  327    ;   Extra = []
  328    ),
  329    option(directory(Dir), Options, .),
  330    setup_call_cleanup(
  331        process_create(path(git),
  332                       [ 'describe',
  333                         '--match', Pattern
  334                       | Extra
  335                       ],
  336                       [ stdout(pipe(Out)),
  337                         stderr(null),
  338                         process(PID),
  339                         cwd(Dir)
  340                       ]),
  341        call_cleanup(
  342            read_stream_to_codes(Out, V0, []),
  343            git_wait(PID, Out, Status)),
  344        close(Out)),
  345    Status = exit(0),
  346    !,
  347    atom_codes(V1, V0),
  348    normalize_space(atom(Plain), V1),
  349    (   git_is_clean(Dir)
  350    ->  Version = Plain
  351    ;   atom_concat(Plain, '-DIRTY', Version)
  352    ).
  353git_describe(Version, Options) :-
  354    option(directory(Dir), Options, .),
  355    option(commit(Commit), Options, 'HEAD'),
  356    setup_call_cleanup(
  357        process_create(path(git),
  358                       [ 'rev-parse', '--short',
  359                         Commit
  360                       ],
  361                       [ stdout(pipe(Out)),
  362                         stderr(null),
  363                         process(PID),
  364                         cwd(Dir)
  365                       ]),
  366        call_cleanup(
  367            read_stream_to_codes(Out, V0, []),
  368            git_wait(PID, Out, Status)),
  369        close(Out)),
  370    Status = exit(0),
  371    atom_codes(V1, V0),
  372    normalize_space(atom(Plain), V1),
  373    (   git_is_clean(Dir)
  374    ->  Version = Plain
  375    ;   atom_concat(Plain, '-DIRTY', Version)
  376    ).
  377
  378
  379:- multifile
  380    git_version_pattern/1.  381
  382git_version_pattern('V*').
  383git_version_pattern('*').
  384
  385
  386%!  git_is_clean(+Dir) is semidet.
  387%
  388%   True if the given directory is in   a git module and this module
  389%   is clean. To us, clean only   implies that =|git diff|= produces
  390%   no output.
  391
  392git_is_clean(Dir) :-
  393    setup_call_cleanup(process_create(path(git), ['diff', '--stat'],
  394                                      [ stdout(pipe(Out)),
  395                                        stderr(null),
  396                                        cwd(Dir)
  397                                      ]),
  398                       stream_char_count(Out, Count),
  399                       close(Out)),
  400    Count == 0.
  401
  402stream_char_count(Out, Count) :-
  403    setup_call_cleanup(open_null_stream(Null),
  404                       (   copy_stream_data(Out, Null),
  405                           character_count(Null, Count)
  406                       ),
  407                       close(Null)).
  408
  409
  410%!  git_hash(-Hash, +Options) is det.
  411%
  412%   Return the hash of the indicated object.
  413
  414git_hash(Hash, Options) :-
  415    option(commit(Commit), Options, 'HEAD'),
  416    git_process_output(['rev-parse', '--verify', Commit],
  417                       read_hash(Hash),
  418                       Options).
  419
  420read_hash(Hash, Stream) :-
  421    read_line_to_codes(Stream, Line),
  422    atom_codes(Hash, Line).
  423
  424
  425%!  git_ls_tree(-Entries, +Options) is det.
  426%
  427%   True  when  Entries  is  a  list  of  entries  in  the  the  GIT
  428%   repository, Each entry is a term:
  429%
  430%     ==
  431%     object(Mode, Type, Hash, Size, Name)
  432%     ==
  433
  434git_ls_tree(Entries, Options) :-
  435    option(commit(Commit), Options, 'HEAD'),
  436    git_process_output(['ls-tree', '-z', '-r', '-l', Commit],
  437                       read_tree(Entries),
  438                       Options).
  439
  440read_tree(Entries, Stream) :-
  441    read_stream_to_codes(Stream, Codes),
  442    phrase(ls_tree(Entries), Codes).
  443
  444ls_tree([H|T]) -->
  445    ls_entry(H),
  446    !,
  447    ls_tree(T).
  448ls_tree([]) --> [].
  449
  450ls_entry(object(Mode, Type, Hash, Size, Name)) -->
  451    string(MS), " ",
  452    string(TS), " ",
  453    string(HS), " ",
  454    string(SS), "\t",
  455    string(NS), [0],
  456    !,
  457    { number_codes(Mode, [0'0,0'o|MS]),
  458      atom_codes(Type, TS),
  459      atom_codes(Hash, HS),
  460      (   Type == blob
  461      ->  number_codes(Size, SS)
  462      ;   Size = 0          % actually '-', but 0 sums easier
  463      ),
  464      atom_codes(Name, NS)
  465    }.
  466
  467
  468%!  git_remote_url(+Remote, -URL, +Options) is det.
  469%
  470%   URL is the remote (fetch) URL for the given Remote.
  471
  472git_remote_url(Remote, URL, Options) :-
  473    git_process_output([remote, show, Remote],
  474                       read_url("Fetch URL:", URL),
  475                       Options).
  476
  477read_url(Tag, URL, In) :-
  478    repeat,
  479        read_line_to_codes(In, Line),
  480        (   Line == end_of_file
  481        ->  !, fail
  482        ;   phrase(url_codes(Tag, Codes), Line)
  483        ->  !, atom_codes(URL, Codes)
  484        ).
  485
  486url_codes(Tag, Rest) -->
  487    { string_codes(Tag, TagCodes) },
  488    whites, string(TagCodes), whites, string(Rest).
  489
  490
  491%!  git_ls_remote(+GitURL, -Refs, +Options) is det.
  492%
  493%   Execute =|git ls-remote|= against the remote repository to fetch
  494%   references from the remote.  Options processed:
  495%
  496%     * heads(Boolean)
  497%     * tags(Boolean)
  498%     * refs(List)
  499%
  500%   For example, to find the hash of the remote =HEAD=, one can use
  501%
  502%     ==
  503%     ?- git_ls_remote('git://www.swi-prolog.org/home/pl/git/pl-devel.git',
  504%                      Refs, [refs(['HEAD'])]).
  505%     Refs = ['5d596c52aa969d88e7959f86327f5c7ff23695f3'-'HEAD'].
  506%     ==
  507%
  508%   @param Refs is a list of pairs hash-name.
  509
  510git_ls_remote(GitURL, Refs, Options) :-
  511    findall(O, ls_remote_option(Options, O), RemoteOptions),
  512    option(refs(LimitRefs), Options, []),
  513    must_be(list(atom), LimitRefs),
  514    append([ 'ls-remote' | RemoteOptions], [GitURL|LimitRefs], Argv),
  515    git_process_output(Argv, remote_refs(Refs), []).
  516
  517ls_remote_option(Options, '--heads') :-
  518    option(heads(true), Options).
  519ls_remote_option(Options, '--tags') :-
  520    option(tags(true), Options).
  521
  522remote_refs(Refs, Out) :-
  523    read_line_to_codes(Out, Line0),
  524    remote_refs(Line0, Out, Refs).
  525
  526remote_refs(end_of_file, _, []) :- !.
  527remote_refs(Line, Out, [Hash-Ref|Tail]) :-
  528    phrase(remote_ref(Hash,Ref), Line),
  529    read_line_to_codes(Out, Line1),
  530    remote_refs(Line1, Out, Tail).
  531
  532remote_ref(Hash, Ref) -->
  533    string_without("\t ", HashCodes),
  534    whites,
  535    string_without("\t ", RefCodes),
  536    { atom_codes(Hash, HashCodes),
  537      atom_codes(Ref, RefCodes)
  538    }.
  539
  540
  541%!  git_remote_branches(+GitURL, -Branches) is det.
  542%
  543%   Exploit git_ls_remote/3 to fetch  the   branches  from  a remote
  544%   repository without downloading it.
  545
  546git_remote_branches(GitURL, Branches) :-
  547    git_ls_remote(GitURL, Refs, [heads(true)]),
  548    findall(B, (member(_-Head, Refs),
  549                atom_concat('refs/heads/', B, Head)),
  550            Branches).
  551
  552
  553%!  git_default_branch(-BranchName, +Options) is det.
  554%
  555%   True when BranchName is the default branch of a repository.
  556
  557git_default_branch(BranchName, Options) :-
  558    git_process_output([branch],
  559                       read_default_branch(BranchName),
  560                       Options).
  561
  562read_default_branch(BranchName, In) :-
  563    repeat,
  564        read_line_to_codes(In, Line),
  565        (   Line == end_of_file
  566        ->  !, fail
  567        ;   phrase(default_branch(Codes), Line)
  568        ->  !, atom_codes(BranchName, Codes)
  569        ).
  570
  571default_branch(Rest) -->
  572    "*", whites, string(Rest).
  573
  574%!  git_branches(-Branches, +Options) is det.
  575%
  576%   True when Branches is the list of branches in the repository.
  577%   In addition to the usual options, this processes:
  578%
  579%     - contains(Commit)
  580%     Return only branches that contain Commit.
  581
  582git_branches(Branches, Options) :-
  583    (   select_option(commit(Commit), Options, GitOptions)
  584    ->  Extra = ['--contains', Commit]
  585    ;   Extra = [],
  586        GitOptions = Options
  587    ),
  588    git_process_output([branch|Extra],
  589                       read_branches(Branches),
  590                       GitOptions).
  591
  592read_branches(Branches, In) :-
  593    read_line_to_codes(In, Line),
  594    (   Line == end_of_file
  595    ->  Branches = []
  596    ;   Line = [_,_|Codes],
  597        atom_codes(H, Codes),
  598        Branches = [H|T],
  599        read_branches(T, In)
  600    ).
  601
  602
  603%!  git_tags_on_branch(+Dir, +Branch, -Tags) is det.
  604%
  605%   Tags is a list of tags in Branch on the GIT repository Dir, most
  606%   recent tag first.
  607%
  608%   @see Git tricks at http://mislav.uniqpath.com/2010/07/git-tips/
  609
  610git_tags_on_branch(Dir, Branch, Tags) :-
  611    git_process_output([ log, '--oneline', '--decorate', Branch ],
  612                       log_to_tags(Tags),
  613                       [ directory(Dir) ]).
  614
  615log_to_tags(Tags, Out) :-
  616    read_line_to_codes(Out, Line0),
  617    log_to_tags(Line0, Out, Tags, []).
  618
  619log_to_tags(end_of_file, _, Tags, Tags) :- !.
  620log_to_tags(Line, Out, Tags, Tail) :-
  621    phrase(tags_on_line(Tags, Tail1), Line),
  622    read_line_to_codes(Out, Line1),
  623    log_to_tags(Line1, Out, Tail1, Tail).
  624
  625tags_on_line(Tags, Tail) -->
  626    string_without(" ", _Hash),
  627    tags(Tags, Tail),
  628    skip_rest.
  629
  630tags(Tags, Tail) -->
  631    whites,
  632    "(",
  633    tag_list(Tags, Rest),
  634    !,
  635    tags(Rest, Tail).
  636tags(Tags, Tags) -->
  637    skip_rest.
  638
  639tag_list([H|T], Rest) -->
  640    "tag:", !, whites,
  641    string(Codes),
  642    (   ")"
  643    ->  { atom_codes(H, Codes),
  644          T = Rest
  645        }
  646    ;   ","
  647    ->  { atom_codes(H, Codes)
  648        },
  649        whites,
  650        tag_list(T, Rest)
  651    ).
  652tag_list(List, Rest) -->
  653    string(_),
  654    (   ")"
  655    ->  { List = Rest }
  656    ;   ","
  657    ->  whites,
  658        tag_list(List, Rest)
  659    ).
  660
  661skip_rest(_,_).
  662
  663
  664                 /*******************************
  665                 *        READ GIT HISTORY      *
  666                 *******************************/
  667
  668%!  git_shortlog(+Dir, -ShortLog, +Options) is det.
  669%
  670%   Fetch information like the  GitWeb   change  overview. Processed
  671%   options:
  672%
  673%       * limit(+Count)
  674%       Maximum number of commits to show (default is 10)
  675%       * revisions(+Revisions)
  676%       Git revision specification
  677%       * path(+Path)
  678%       Only show commits that affect Path.  Path is the path of
  679%       a checked out file.
  680%       * git_path(+Path)
  681%       Similar to =path=, but Path is relative to the repository.
  682%
  683%   @param ShortLog is a list of =git_log= records.
  684
  685:- record
  686    git_log(commit_hash:atom,
  687            author_name:atom,
  688            author_date_relative:atom,
  689            committer_name:atom,
  690            committer_date_relative:atom,
  691            committer_date_unix:integer,
  692            subject:atom,
  693            ref_names:list).  694
  695git_shortlog(Dir, ShortLog, Options) :-
  696    (   option(revisions(Range), Options)
  697    ->  RangeSpec = [Range]
  698    ;   option(limit(Limit), Options, 10),
  699        RangeSpec = ['-n', Limit]
  700    ),
  701    (   option(git_path(Path), Options)
  702    ->  Extra = ['--', Path]
  703    ;   option(path(Path), Options)
  704    ->  relative_file_name(Path, Dir, RelPath),
  705        Extra = ['--', RelPath]
  706    ;   Extra = []
  707    ),
  708    git_format_string(git_log, Fields, Format),
  709    append([[log, Format], RangeSpec, Extra], GitArgv),
  710    git_process_output(GitArgv,
  711                       read_git_formatted(git_log, Fields, ShortLog),
  712                       [directory(Dir)]).
  713
  714
  715read_git_formatted(Record, Fields, ShortLog, In) :-
  716    read_line_to_codes(In, Line0),
  717    read_git_formatted(Line0, In, Record, Fields, ShortLog).
  718
  719read_git_formatted(end_of_file, _, _, _, []) :- !.
  720read_git_formatted(Line, In, Record, Fields, [H|T]) :-
  721    record_from_line(Record, Fields, Line, H),
  722    read_line_to_codes(In, Line1),
  723    read_git_formatted(Line1, In, Record, Fields, T).
  724
  725record_from_line(RecordName, Fields, Line, Record) :-
  726    phrase(fields_from_line(Fields, Values), Line),
  727    Record =.. [RecordName|Values].
  728
  729fields_from_line([], []) --> [].
  730fields_from_line([F|FT], [V|VT]) -->
  731    to_nul_s(Codes),
  732    { field_to_prolog(F, Codes, V) },
  733    fields_from_line(FT, VT).
  734
  735to_nul_s([]) --> [0], !.
  736to_nul_s([H|T]) --> [H], to_nul_s(T).
  737
  738field_to_prolog(ref_names, Line, List) :-
  739    phrase(ref_names(List), Line),
  740    !.
  741field_to_prolog(committer_date_unix, Line, Stamp) :-
  742    !,
  743    number_codes(Stamp, Line).
  744field_to_prolog(_, Line, Atom) :-
  745    atom_codes(Atom, Line).
  746
  747ref_names([]) --> [].
  748ref_names(List) -->
  749    blanks, "(", ref_name_list(List), ")".
  750
  751ref_name_list([H|T]) -->
  752    string_without(",)", Codes),
  753    { atom_codes(H, Codes) },
  754    (   ",", blanks
  755    ->  ref_name_list(T)
  756    ;   {T=[]}
  757    ).
  758
  759
  760%!  git_show(+Dir, +Hash, -Commit, +Options) is det.
  761%
  762%   Fetch info from a GIT commit.  Options processed:
  763%
  764%     * diff(Diff)
  765%     GIT option on how to format diffs.  E.g. =stat=
  766%     * max_lines(Count)
  767%     Truncate the body at Count lines.
  768%
  769%   @param  Commit is a term git_commit(...)-Body.  Body is currently
  770%           a list of lines, each line represented as a list of
  771%           codes.
  772
  773:- record
  774    git_commit(tree_hash:atom,
  775               parent_hashes:list,
  776               author_name:atom,
  777               author_date:atom,
  778               committer_name:atom,
  779               committer_date:atom,
  780               subject:atom).  781
  782git_show(Dir, Hash, Commit, Options) :-
  783    git_format_string(git_commit, Fields, Format),
  784    option(diff(Diff), Options, patch),
  785    diff_arg(Diff, DiffArg),
  786    git_process_output([ show, DiffArg, Hash, Format ],
  787                       read_commit(Fields, Commit, Options),
  788                       [directory(Dir)]).
  789
  790diff_arg(patch, '-p').
  791diff_arg(stat, '--stat').
  792
  793read_commit(Fields, Data-Body, Options, In) :-
  794    read_line_to_codes(In, Line1),
  795    record_from_line(git_commit, Fields, Line1, Data),
  796    read_line_to_codes(In, Line2),
  797    (   Line2 == []
  798    ->  option(max_lines(Max), Options, -1),
  799        read_n_lines(In, Max, Body)
  800    ;   Line2 == end_of_file
  801    ->  Body = []
  802    ).
  803
  804read_n_lines(In, Max, Lines) :-
  805    read_line_to_codes(In, Line1),
  806    read_n_lines(Line1, Max, In, Lines).
  807
  808read_n_lines(end_of_file, _, _, []) :- !.
  809read_n_lines(_, 0, In, []) :-
  810    !,
  811    setup_call_cleanup(open_null_stream(Out),
  812                       copy_stream_data(In, Out),
  813                       close(Out)).
  814read_n_lines(Line, Max0, In, [Line|More]) :-
  815    read_line_to_codes(In, Line2),
  816    Max is Max0-1,
  817    read_n_lines(Line2, Max, In, More).
  818
  819
  820%!  git_format_string(:Record, -FieldNames, -Format)
  821%
  822%   If Record is a record with  fields   whose  names  match the GIT
  823%   format field-names, Format is a  git =|--format=|= argument with
  824%   the appropriate format-specifiers,  terminated   by  %x00, which
  825%   causes the actual field to be 0-terminated.
  826
  827:- meta_predicate
  828    git_format_string(:, -, -).  829
  830git_format_string(M:RecordName, Fields, Format) :-
  831    current_record(RecordName, M:Term),
  832    findall(F, record_field(Term, F), Fields),
  833    maplist(git_field_format, Fields, Formats),
  834    atomic_list_concat(['--format='|Formats], Format).
  835
  836record_field(Term, Name) :-
  837    arg(_, Term, Field),
  838    field_name(Field, Name).
  839
  840field_name(Name:_Type=_Default, Name) :- !.
  841field_name(Name:_Type, Name) :- !.
  842field_name(Name=_Default, Name) :- !.
  843field_name(Name, Name).
  844
  845git_field_format(Field, Fmt) :-
  846    (   git_format(NoPercent, Field)
  847    ->  atomic_list_concat(['%', NoPercent, '%x00'], Fmt)
  848    ;   existence_error(git_format, Field)
  849    ).
  850
  851git_format('H', commit_hash).
  852git_format('h', abbreviated_commit_hash).
  853git_format('T', tree_hash).
  854git_format('t', abbreviated_tree_hash).
  855git_format('P', parent_hashes).
  856git_format('p', abbreviated_parent_hashes).
  857
  858git_format('an', author_name).
  859git_format('aN', author_name_mailcap).
  860git_format('ae', author_email).
  861git_format('aE', author_email_mailcap).
  862git_format('ad', author_date).
  863git_format('aD', author_date_rfc2822).
  864git_format('ar', author_date_relative).
  865git_format('at', author_date_unix).
  866git_format('ai', author_date_iso8601).
  867
  868git_format('cn', committer_name).
  869git_format('cN', committer_name_mailcap).
  870git_format('ce', committer_email).
  871git_format('cE', committer_email_mailcap).
  872git_format('cd', committer_date).
  873git_format('cD', committer_date_rfc2822).
  874git_format('cr', committer_date_relative).
  875git_format('ct', committer_date_unix).
  876git_format('ci', committer_date_iso8601).
  877
  878git_format('d', ref_names).             % git log?
  879git_format('e', encoding).              % git log?
  880
  881git_format('s', subject).
  882git_format('f', subject_sanitized).
  883git_format('b', body).
  884git_format('N', notes).
  885
  886git_format('gD', reflog_selector).
  887git_format('gd', shortened_reflog_selector).
  888git_format('gs', reflog_subject).
  889
  890
  891                 /*******************************
  892                 *            MESSAGES          *
  893                 *******************************/
  894
  895:- multifile
  896    prolog:message//1.  897
  898prolog:message(git(output(Codes))) -->
  899    { split_lines(Codes, Lines) },
  900    git_lines(Lines).
  901
  902git_lines([]) --> [].
  903git_lines([H|T]) -->
  904    [ '~s'-[H] ],
  905    (   {T==[]}
  906    ->  []
  907    ;   [nl], git_lines(T)
  908    ).
  909
  910split_lines([], []) :- !.
  911split_lines(All, [Line1|More]) :-
  912    append(Line1, [0'\n|Rest], All),
  913    !,
  914    split_lines(Rest, More).
  915split_lines(Line, [Line])