35
36:- module(git,
37 [ git/2, 38 git_process_output/3, 39 git_open_file/4, 40 is_git_directory/1, 41 git_describe/2, 42 git_hash/2, 43 git_ls_tree/2, 44 git_remote_url/3, 45 git_ls_remote/3, 46 git_branches/2, 47 git_remote_branches/2, 48 git_default_branch/2, 49 git_tags_on_branch/3, 50 git_shortlog/3, 51 git_log_data/3, 52 git_show/4, 53 git_commit_data/3 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
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
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
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
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
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
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
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
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
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
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 463 ),
464 atom_codes(Name, NS)
465 }.
466
467
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
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
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
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
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
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 667
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
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
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). 879git_format('e', encoding). 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 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])