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