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 [ limit(nonneg),
110 path(atom)
111 ]).
112:- predicate_options(git_show/4, 4,
113 [ diff(oneof([patch,stat]))
114 ]).
115
116
131
132git(Argv, Options) :-
133 option(directory(Dir), Options, .),
134 env_options(Extra, Options),
135 setup_call_cleanup(
136 process_create(path(git), Argv,
137 [ stdout(pipe(Out)),
138 stderr(pipe(Error)),
139 process(PID),
140 cwd(Dir)
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
158env_options([env(['GIT_ASKPASS'=Program])], Options) :-
159 option(askpass(Exe), Options),
160 !,
161 exe_options(ExeOptions),
162 absolute_file_name(Exe, PlProg, ExeOptions),
163 prolog_to_os_filename(PlProg, Program).
164env_options([], _).
165
166exe_options(Options) :-
167 current_prolog_flag(windows, true),
168 !,
169 Options = [ extensions(['',exe,com]), access(read) ].
170exe_options(Options) :-
171 Options = [ access(execute) ].
172
173print_output(OutCodes, Options) :-
174 option(output(Codes), Options),
175 !,
176 Codes = OutCodes.
177print_output([], _) :- !.
178print_output(OutCodes, _) :-
179 print_message(informational, git(output(OutCodes))).
180
181print_error(OutCodes, Options) :-
182 option(error(Codes), Options),
183 !,
184 Codes = OutCodes.
185print_error([], _) :- !.
186print_error(OutCodes, _) :-
187 phrase(classify_message(Level), OutCodes, _),
188 print_message(Level, git(output(OutCodes))).
189
190classify_message(error) -->
191 string(_), "fatal:",
192 !.
193classify_message(error) -->
194 string(_), "error:",
195 !.
196classify_message(warning) -->
197 string(_), "warning:",
198 !.
199classify_message(informational) -->
200 [].
201
206
207close_streams(List) :-
208 phrase(close_streams(List), Errors),
209 ( Errors = [Error|_]
210 -> throw(Error)
211 ; true
212 ).
213
214close_streams([H|T]) -->
215 { catch(close(H), E, true) },
216 ( { var(E) }
217 -> []
218 ; [E]
219 ),
220 close_streams(T).
221
222
227
228git_process_output(Argv, OnOutput, Options) :-
229 option(directory(Dir), Options, .),
230 env_options(Extra, Options),
231 setup_call_cleanup(
232 process_create(path(git), Argv,
233 [ stdout(pipe(Out)),
234 stderr(pipe(Error)),
235 process(PID),
236 cwd(Dir)
237 | Extra
238 ]),
239 call_cleanup(
240 ( call(OnOutput, Out),
241 read_stream_to_codes(Error, ErrorCodes, [])
242 ),
243 process_wait(PID, Status)),
244 close_streams([Out,Error])),
245 print_error(ErrorCodes, Options),
246 ( Status = exit(0)
247 -> true
248 ; throw(error(process_error(git, Status)))
249 ).
250
251
258
259git_open_file(Dir, File, Branch, In) :-
260 atomic_list_concat([Branch, :, File], Ref),
261 process_create(path(git),
262 [ show, Ref ],
263 [ stdout(pipe(In)),
264 cwd(Dir)
265 ]),
266 set_stream(In, file_name(File)).
267
268
273
274is_git_directory(Directory) :-
275 directory_file_path(Directory, '.git', GitDir),
276 exists_directory(GitDir),
277 !.
278is_git_directory(Directory) :-
279 exists_directory(Directory),
280 git(['rev-parse', '--git-dir'],
281 [ output(Codes),
282 error(_),
283 status(Status),
284 directory(Directory)
285 ]),
286 Status == exit(0),
287 string_codes(".\n", Codes).
288
304
305git_describe(Version, Options) :-
306 ( option(match(Pattern), Options)
307 -> true
308 ; git_version_pattern(Pattern)
309 ),
310 ( option(commit(Commit), Options)
311 -> Extra = [Commit]
312 ; Extra = []
313 ),
314 option(directory(Dir), Options, .),
315 setup_call_cleanup(
316 process_create(path(git),
317 [ 'describe',
318 '--match', Pattern
319 | Extra
320 ],
321 [ stdout(pipe(Out)),
322 stderr(null),
323 process(PID),
324 cwd(Dir)
325 ]),
326 call_cleanup(
327 read_stream_to_codes(Out, V0, []),
328 process_wait(PID, Status)),
329 close(Out)),
330 Status = exit(0),
331 !,
332 atom_codes(V1, V0),
333 normalize_space(atom(Plain), V1),
334 ( git_is_clean(Dir)
335 -> Version = Plain
336 ; atom_concat(Plain, '-DIRTY', Version)
337 ).
338git_describe(Version, Options) :-
339 option(directory(Dir), Options, .),
340 option(commit(Commit), Options, 'HEAD'),
341 setup_call_cleanup(
342 process_create(path(git),
343 [ 'rev-parse', '--short',
344 Commit
345 ],
346 [ stdout(pipe(Out)),
347 stderr(null),
348 process(PID),
349 cwd(Dir)
350 ]),
351 call_cleanup(
352 read_stream_to_codes(Out, V0, []),
353 process_wait(PID, Status)),
354 close(Out)),
355 Status = exit(0),
356 atom_codes(V1, V0),
357 normalize_space(atom(Plain), V1),
358 ( git_is_clean(Dir)
359 -> Version = Plain
360 ; atom_concat(Plain, '-DIRTY', Version)
361 ).
362
363
364:- multifile
365 git_version_pattern/1.
366
367git_version_pattern('V*').
368git_version_pattern('*').
369
370
376
377git_is_clean(Dir) :-
378 setup_call_cleanup(process_create(path(git), ['diff', '--stat'],
379 [ stdout(pipe(Out)),
380 stderr(null),
381 cwd(Dir)
382 ]),
383 stream_char_count(Out, Count),
384 close(Out)),
385 Count == 0.
386
387stream_char_count(Out, Count) :-
388 setup_call_cleanup(open_null_stream(Null),
389 ( copy_stream_data(Out, Null),
390 character_count(Null, Count)
391 ),
392 close(Null)).
393
394
398
399git_hash(Hash, Options) :-
400 option(commit(Commit), Options, 'HEAD'),
401 git_process_output(['rev-parse', '--verify', Commit],
402 read_hash(Hash),
403 Options).
404
405read_hash(Hash, Stream) :-
406 read_line_to_codes(Stream, Line),
407 atom_codes(Hash, Line).
408
409
418
419git_ls_tree(Entries, Options) :-
420 option(commit(Commit), Options, 'HEAD'),
421 git_process_output(['ls-tree', '-z', '-r', '-l', Commit],
422 read_tree(Entries),
423 Options).
424
425read_tree(Entries, Stream) :-
426 read_stream_to_codes(Stream, Codes),
427 phrase(ls_tree(Entries), Codes).
428
429ls_tree([H|T]) -->
430 ls_entry(H),
431 !,
432 ls_tree(T).
433ls_tree([]) --> [].
434
435ls_entry(object(Mode, Type, Hash, Size, Name)) -->
436 string(MS), " ",
437 string(TS), " ",
438 string(HS), " ",
439 string(SS), "\t",
440 string(NS), [0],
441 !,
442 { number_codes(Mode, [0'0,0'o|MS]),
443 atom_codes(Type, TS),
444 atom_codes(Hash, HS),
445 ( Type == blob
446 -> number_codes(Size, SS)
447 ; Size = 0 448 ),
449 atom_codes(Name, NS)
450 }.
451
452
456
457git_remote_url(Remote, URL, Options) :-
458 git_process_output([remote, show, Remote],
459 read_url("Fetch URL:", URL),
460 Options).
461
462read_url(Tag, URL, In) :-
463 repeat,
464 read_line_to_codes(In, Line),
465 ( Line == end_of_file
466 -> !, fail
467 ; phrase(url_codes(Tag, Codes), Line)
468 -> !, atom_codes(URL, Codes)
469 ).
470
471url_codes(Tag, Rest) -->
472 { string_codes(Tag, TagCodes) },
473 whites, string(TagCodes), whites, string(Rest).
474
475
494
495git_ls_remote(GitURL, Refs, Options) :-
496 findall(O, ls_remote_option(Options, O), RemoteOptions),
497 option(refs(LimitRefs), Options, []),
498 must_be(list(atom), LimitRefs),
499 append([ 'ls-remote' | RemoteOptions], [GitURL|LimitRefs], Argv),
500 git_process_output(Argv, remote_refs(Refs), []).
501
502ls_remote_option(Options, '--heads') :-
503 option(heads(true), Options).
504ls_remote_option(Options, '--tags') :-
505 option(tags(true), Options).
506
507remote_refs(Refs, Out) :-
508 read_line_to_codes(Out, Line0),
509 remote_refs(Line0, Out, Refs).
510
511remote_refs(end_of_file, _, []) :- !.
512remote_refs(Line, Out, [Hash-Ref|Tail]) :-
513 phrase(remote_ref(Hash,Ref), Line),
514 read_line_to_codes(Out, Line1),
515 remote_refs(Line1, Out, Tail).
516
517remote_ref(Hash, Ref) -->
518 string_without("\t ", HashCodes),
519 whites,
520 string_without("\t ", RefCodes),
521 { atom_codes(Hash, HashCodes),
522 atom_codes(Ref, RefCodes)
523 }.
524
525
530
531git_remote_branches(GitURL, Branches) :-
532 git_ls_remote(GitURL, Refs, [heads(true)]),
533 findall(B, (member(_-Head, Refs),
534 atom_concat('refs/heads/', B, Head)),
535 Branches).
536
537
541
542git_default_branch(BranchName, Options) :-
543 git_process_output([branch],
544 read_default_branch(BranchName),
545 Options).
546
547read_default_branch(BranchName, In) :-
548 repeat,
549 read_line_to_codes(In, Line),
550 ( Line == end_of_file
551 -> !, fail
552 ; phrase(default_branch(Codes), Line)
553 -> !, atom_codes(BranchName, Codes)
554 ).
555
556default_branch(Rest) -->
557 "*", whites, string(Rest).
558
566
567git_branches(Branches, Options) :-
568 ( select_option(commit(Commit), Options, GitOptions)
569 -> Extra = ['--contains', Commit]
570 ; Extra = [],
571 GitOptions = Options
572 ),
573 git_process_output([branch|Extra],
574 read_branches(Branches),
575 GitOptions).
576
577read_branches(Branches, In) :-
578 read_line_to_codes(In, Line),
579 ( Line == end_of_file
580 -> Branches = []
581 ; Line = [_,_|Codes],
582 atom_codes(H, Codes),
583 Branches = [H|T],
584 read_branches(T, In)
585 ).
586
587
594
595git_tags_on_branch(Dir, Branch, Tags) :-
596 git_process_output([ log, '--oneline', '--decorate', Branch ],
597 log_to_tags(Tags),
598 [ directory(Dir) ]).
599
600log_to_tags(Tags, Out) :-
601 read_line_to_codes(Out, Line0),
602 log_to_tags(Line0, Out, Tags, []).
603
604log_to_tags(end_of_file, _, Tags, Tags) :- !.
605log_to_tags(Line, Out, Tags, Tail) :-
606 phrase(tags_on_line(Tags, Tail1), Line),
607 read_line_to_codes(Out, Line1),
608 log_to_tags(Line1, Out, Tail1, Tail).
609
610tags_on_line(Tags, Tail) -->
611 string_without(" ", _Hash),
612 tags(Tags, Tail),
613 skip_rest.
614
615tags(Tags, Tail) -->
616 whites,
617 "(",
618 tag_list(Tags, Rest),
619 !,
620 tags(Rest, Tail).
621tags(Tags, Tags) -->
622 skip_rest.
623
624tag_list([H|T], Rest) -->
625 "tag:", !, whites,
626 string(Codes),
627 ( ")"
628 -> { atom_codes(H, Codes),
629 T = Rest
630 }
631 ; ","
632 -> { atom_codes(H, Codes)
633 },
634 whites,
635 tag_list(T, Rest)
636 ).
637tag_list(List, Rest) -->
638 string(_),
639 ( ")"
640 -> { List = Rest }
641 ; ","
642 -> whites,
643 tag_list(List, Rest)
644 ).
645
646skip_rest(_,_).
647
648
649 652
667
668:- record
669 git_log(commit_hash:atom,
670 author_name:atom,
671 author_date_relative:atom,
672 committer_name:atom,
673 committer_date_relative:atom,
674 committer_date_unix,
675 subject:atom,
676 ref_names:list).
677
678git_shortlog(Dir, ShortLog, Options) :-
679 option(limit(Limit), Options, 10),
680 ( option(git_path(Path), Options)
681 -> Extra = ['--', Path]
682 ; option(path(Path), Options)
683 -> relative_file_name(Path, Dir, RelPath),
684 Extra = ['--', RelPath]
685 ; Extra = []
686 ),
687 git_format_string(git_log, Fields, Format),
688 git_process_output([ log, '-n', Limit, Format
689 | Extra
690 ],
691 read_git_formatted(git_log, Fields, ShortLog),
692 [directory(Dir)]).
693
694
695read_git_formatted(Record, Fields, ShortLog, In) :-
696 read_line_to_codes(In, Line0),
697 read_git_formatted(Line0, In, Record, Fields, ShortLog).
698
699read_git_formatted(end_of_file, _, _, _, []) :- !.
700read_git_formatted(Line, In, Record, Fields, [H|T]) :-
701 record_from_line(Record, Fields, Line, H),
702 read_line_to_codes(In, Line1),
703 read_git_formatted(Line1, In, Record, Fields, T).
704
705record_from_line(RecordName, Fields, Line, Record) :-
706 phrase(fields_from_line(Fields, Values), Line),
707 Record =.. [RecordName|Values].
708
709fields_from_line([], []) --> [].
710fields_from_line([F|FT], [V|VT]) -->
711 to_nul_s(Codes),
712 { field_to_prolog(F, Codes, V) },
713 fields_from_line(FT, VT).
714
715to_nul_s([]) --> [0], !.
716to_nul_s([H|T]) --> [H], to_nul_s(T).
717
718field_to_prolog(ref_names, Line, List) :-
719 phrase(ref_names(List), Line),
720 !.
721field_to_prolog(_, Line, Atom) :-
722 atom_codes(Atom, Line).
723
724ref_names([]) --> [].
725ref_names(List) -->
726 blanks, "(", ref_name_list(List), ")".
727
728ref_name_list([H|T]) -->
729 string_without(",)", Codes),
730 { atom_codes(H, Codes) },
731 ( ",", blanks
732 -> ref_name_list(T)
733 ; {T=[]}
734 ).
735
736
749
750:- record
751 git_commit(tree_hash:atom,
752 parent_hashes:list,
753 author_name:atom,
754 author_date:atom,
755 committer_name:atom,
756 committer_date:atom,
757 subject:atom).
758
759git_show(Dir, Hash, Commit, Options) :-
760 git_format_string(git_commit, Fields, Format),
761 option(diff(Diff), Options, patch),
762 diff_arg(Diff, DiffArg),
763 git_process_output([ show, DiffArg, Hash, Format ],
764 read_commit(Fields, Commit, Options),
765 [directory(Dir)]).
766
767diff_arg(patch, '-p').
768diff_arg(stat, '--stat').
769
770read_commit(Fields, Data-Body, Options, In) :-
771 read_line_to_codes(In, Line1),
772 record_from_line(git_commit, Fields, Line1, Data),
773 read_line_to_codes(In, Line2),
774 ( Line2 == []
775 -> option(max_lines(Max), Options, -1),
776 read_n_lines(In, Max, Body)
777 ; Line2 == end_of_file
778 -> Body = []
779 ).
780
781read_n_lines(In, Max, Lines) :-
782 read_line_to_codes(In, Line1),
783 read_n_lines(Line1, Max, In, Lines).
784
785read_n_lines(end_of_file, _, _, []) :- !.
786read_n_lines(_, 0, In, []) :-
787 !,
788 setup_call_cleanup(open_null_stream(Out),
789 copy_stream_data(In, Out),
790 close(Out)).
791read_n_lines(Line, Max0, In, [Line|More]) :-
792 read_line_to_codes(In, Line2),
793 Max is Max0-1,
794 read_n_lines(Line2, Max, In, More).
795
796
803
804:- meta_predicate
805 git_format_string(:, -, -).
806
807git_format_string(M:RecordName, Fields, Format) :-
808 current_record(RecordName, M:Term),
809 findall(F, record_field(Term, F), Fields),
810 maplist(git_field_format, Fields, Formats),
811 atomic_list_concat(['--format='|Formats], Format).
812
813record_field(Term, Name) :-
814 arg(_, Term, Field),
815 field_name(Field, Name).
816
817field_name(Name:_Type=_Default, Name) :- !.
818field_name(Name:_Type, Name) :- !.
819field_name(Name=_Default, Name) :- !.
820field_name(Name, Name).
821
822git_field_format(Field, Fmt) :-
823 ( git_format(NoPercent, Field)
824 -> atomic_list_concat(['%', NoPercent, '%x00'], Fmt)
825 ; existence_error(git_format, Field)
826 ).
827
828git_format('H', commit_hash).
829git_format('h', abbreviated_commit_hash).
830git_format('T', tree_hash).
831git_format('t', abbreviated_tree_hash).
832git_format('P', parent_hashes).
833git_format('p', abbreviated_parent_hashes).
834
835git_format('an', author_name).
836git_format('aN', author_name_mailcap).
837git_format('ae', author_email).
838git_format('aE', author_email_mailcap).
839git_format('ad', author_date).
840git_format('aD', author_date_rfc2822).
841git_format('ar', author_date_relative).
842git_format('at', author_date_unix).
843git_format('ai', author_date_iso8601).
844
845git_format('cn', committer_name).
846git_format('cN', committer_name_mailcap).
847git_format('ce', committer_email).
848git_format('cE', committer_email_mailcap).
849git_format('cd', committer_date).
850git_format('cD', committer_date_rfc2822).
851git_format('cr', committer_date_relative).
852git_format('ct', committer_date_unix).
853git_format('ci', committer_date_iso8601).
854
855git_format('d', ref_names). 856git_format('e', encoding). 857
858git_format('s', subject).
859git_format('f', subject_sanitized).
860git_format('b', body).
861git_format('N', notes).
862
863git_format('gD', reflog_selector).
864git_format('gd', shortened_reflog_selector).
865git_format('gs', reflog_subject).
866
867
868 871
872:- multifile
873 prolog:message//1.
874
875prolog:message(git(output(Codes))) -->
876 { split_lines(Codes, Lines) },
877 git_lines(Lines).
878
879git_lines([]) --> [].
880git_lines([H|T]) -->
881 [ '~s'-[H] ],
882 ( {T==[]}
883 -> []
884 ; [nl], git_lines(T)
885 ).
886
887split_lines([], []) :- !.
888split_lines(All, [Line1|More]) :-
889 append(Line1, [0'\n|Rest], All),
890 !,
891 split_lines(Rest, More).
892split_lines(Line, [Line]).