-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathporcelain.ml
451 lines (381 loc) · 14.9 KB
/
porcelain.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
(** A programmer friendly wrapper of git system commands *)
open Plumbing
module type Porcelain = sig
(** Representation of porcelain git commands.
This module represents the calling of calling git commands that
the user interface will find more useful than using the raw
plumbing commands *)
(** The abstract type of a git commit object *)
type commit_t
(** The type identifying any object *)
type object_id = string
(** The abstract type containing the contents of a blob, tree, commit,
or tag *)
type object_content
(** The abstract type determining whether an object is a blob, tree,
commit, or tag *)
type object_type
(** The abstract type represeting the current git state *)
type status_t
(** [init d] initializes a git repository in the current working
directory if [n] is [None], otherwise initializes a git repository
with name [n] as a subdirectory in the current working directory *)
val init : string option -> unit
(** [pull] pulls files from repository *)
val pull : string -> string -> string -> string
(** [push] pushes files to the repository *)
val push : string -> string -> string -> string
(** [log h] is the list of commit objects that are reachable from HEAD
in reverse chronological order if [h] is [None], otherwise the
commit objects that are reachable by following parents of commit
[h] in reverse chronological order *)
val log : object_id option -> commit_t list
(** [add fnames] adds the files with filenames [fnames] to the staging
area *)
val add : string list -> unit
(** [branch_msg n] is the message of the last commit in the branch
named [n] *)
val branch_msg : string -> string
(** [restore_staged fnames] restores staged files [fnames] from the
staging area *)
val restore_staged : string list -> unit
(** [commit msg] commits the changes in the staging area with commit
message [msg] *)
val commit : string -> string
(** [diff] shows the diffs of tracked files *)
val diff : unit -> string
(** [status] shows the status of the working tree *)
val status : unit -> status_t
(** [checkout b] switches to branch named [b] *)
val checkout : string -> string
(** [create_branch b] creates a new branch with name [b] *)
val create_branch : string -> string
(** [delete_branch b] deletes the branch named [b] *)
val delete_branch : string -> string
(** [stash_apply] applies changes to the current working tree, leaving
them on the stash stack. *)
val stash_apply : unit -> string
(** [stash_pop] applies changes to the current working tree and
removes them from the stash stack. *)
val stash_pop : unit -> string
(** [reset_hard c] puts the head at the commit with hash [c], deleting
any changes made after [c]. *)
val reset_hard : string -> string
(** [reset_soft c] puts the head at the commit with has [c], but keeps
all changes made after [c]. *)
val reset_soft : string -> string
(** [string_of_commit c] is a commit in the form [hash msg] *)
val string_of_commit_t : commit_t -> string
(** [get_untracked s] is the untracked filenames in the status [s] *)
val get_untracked : status_t -> string list
(** [get_tracked s] is the untracked filenames in the status [s] *)
val get_tracked : status_t -> string list
(** [get_staged s] is the staged filenames in the status [s] *)
val get_staged : status_t -> string list
end
(** The porcelain used when running OGit *)
module PorcelainImpl (P : Plumbing) = struct
(** The abstract type of a git commit object in [PorcelainImpl]*)
type commit_t = {
tree : string;
(*parents : string;*)
(*author : string;*)
(*committer : string;*)
msg : string;
}
(** The type identifying any object in [PorcelainImpl]*)
type object_id = string
(** The abstract type containing the contents of a blob, tree, commit,
or tag in [PorcelainImpl]*)
type object_content = unit
(** The abstract type determining whether an object is a blob, tree,
commit, or tag in [PorcelainImpl]*)
type object_type =
| Blob of { contents : string }
| Tree of {
entry : int;
ob_type : object_type;
sha1 : string;
name : string;
}
| Commit of { com_ob : commit_t }
| Tag of {
obj_name : string;
(*?*)
ob_type : object_type;
tagger : string;
msg : string;
}
(** The abstract type represeting the current git state in
[PorcelainImpl]*)
type status_t = {
untracked : string list;
tracked : string list;
staged : string list;
}
(**[rm_leading_spaces str] returns [str] with any leading spaces
removed. All characters after the first non-space character are
left intact.*)
let rec rm_leading_spaces str =
match String.split_on_char ' ' str with
| [] -> str
| [ "" ] -> str
| "" :: t ->
rm_leading_spaces (String.sub str 0 (String.length str - 1))
| h :: t -> str
(** [pull u p b] pulls files from branch [b]. [u] is the username and
[p] is the password of the user. *)
let pull u p b =
match b with
| "remote" ->
P.pull [||]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
| branch ->
P.pull [| branch |]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
(** [push] pushes files to branch [b]. [u] is the username and [p] is
the password of the user *)
let push u p b =
match b with
| "remote" ->
P.push [||]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
| branch ->
P.push [| "origin"; branch |]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
(**[contains] returns true if string [s1] contains string [s2]*)
let contains s1 s2 =
let re = Str.regexp_string s2 in
try
ignore (Str.search_forward re s1 0);
true
with Not_found -> false
(**[commit_t_of_commit_oneline line] converts [line] to a printable
output*)
let commit_t_of_commit_oneline line =
let hash =
if contains line "fatal:" then "" else String.sub line 0 7
in
let msg =
try " " ^ String.sub line 9 (String.length line - 22)
with Invalid_argument _ -> hash ^ " "
in
{ tree = hash; msg }
(** [commit_t_list_of_res res] converts [res] to a list of commits*)
let commit_t_list_of_res res =
let lines = P.get_out res in
List.map commit_t_of_commit_oneline lines
|> List.filter (fun x -> x.tree <> "")
(** [log h] is the list of commit objects that are reachable from HEAD
in reverse chronological order if [h] is [None], otherwise the
commit objects that are reachable by following parents of commit
[h] in reverse chronological order *)
let log hash =
match hash with
| None ->
let res = P.log [| "-10" |] in
commit_t_list_of_res res
| Some h ->
let res = P.log [| h; "-10" |] in
commit_t_list_of_res res
(**[branch_msg name] displays the git output message of an operation *)
let branch_msg name =
if contains name "fatal:" then ""
else
try
let res = P.log [| "--graph"; name; "-1"; "--format=%s" |] in
let msg =
P.get_out res |> List.fold_left (fun acc x -> acc ^ x) ""
in
let start = String.index msg '*' in
String.sub msg (start + 2) (String.length msg - start - 2)
with Not_found -> ""
(**[get_head] returns the current head commit*)
let get_head () =
let long_ref =
match P.get_out (P.head [||]) with [] -> "" | h :: t -> h
in
let start =
match long_ref with
| "" -> 0
| _ -> (
try
Str.search_backward (Str.regexp "heads") long_ref
(String.length long_ref - 1)
+ 6
with Not_found -> 0)
in
String.sub long_ref start (String.length long_ref - start)
(**[get_last_msg] returns the first line of the log*)
let get_last_msg =
P.get_out (P.log [| "-1"; "--format=%s" |])
|> List.fold_left (fun acc x -> acc ^ x) ""
(**[get_upstream] returns the upstream commit*)
let get_upstream () =
P.get_out
(P.revparse
[| "--abbrev-ref"; "--symbolic-full-name"; "@{upstream}" |])
|> List.fold_left (fun acc x -> acc ^ x) ""
(**[get_push] returns the push commit*)
let get_push () =
P.get_out
(P.revparse
[| "--abbrev-ref"; "--symbolic-full-name"; "@{push}" |])
|> List.fold_left (fun acc x -> acc ^ x) ""
(** [add fnames] adds the files with filenames [fnames] to the staging
area *)
let add files =
let args_arr = Array.of_list files in
ignore (P.add args_arr)
(**[restore_staged files] unstages [files]*)
let restore_staged files =
let args_lst = "--staged" :: files in
let args_arr = Array.of_list args_lst in
ignore (P.restore args_arr)
(** [commit msg] commits the changes in the staging area with commit
message [msg] *)
let commit msg =
P.commit [| "-m"; msg |]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
(** [diff] shows the diffs of tracked files *)
let diff () =
P.diff [||]
|> P.get_out
|> List.map rm_leading_spaces
|> List.rev |> String.concat "\n"
(**[empty_status_t] is a status with no untracked, tracked, or staged
files *)
let empty_status_t = { untracked = []; tracked = []; staged = [] }
(**[add_to_untracked status filename] adds [filename] to the list of
untracked files and updates [status]*)
let add_to_untracked status filename =
{
tracked = status.tracked;
untracked = filename :: status.untracked;
staged = status.staged;
}
(**[add_to_tracked status filename] tracks [filename] and updates
[status]*)
let add_to_tracked status filename =
{
tracked = filename :: status.tracked;
untracked = status.untracked;
staged = status.staged;
}
(**[add_to_staged status filename] stages [filename] and updates
[status]*)
let add_to_staged status filename =
{
tracked = status.tracked;
untracked = status.untracked;
staged = filename :: status.staged;
}
(**[add_to_staged status filename] tracks and stages [filename] and
updates [status]*)
let add_to_staged_and_tracked status filename =
let status' = add_to_staged status filename in
add_to_tracked status' filename
(**[add_to_status_t status line] adds [line] to the correct lists
based on its filename, then updates [status]*)
let add_to_status_t status line =
let filename = String.sub line 2 (String.length line - 2) in
let filename = String.trim filename in
match String.sub line 0 2 with
| "??" -> add_to_untracked status filename
| " M" -> add_to_tracked status filename
| "M " -> add_to_staged status filename
| "MM" -> add_to_staged_and_tracked status filename
| "MD" -> add_to_staged_and_tracked status filename
| " A" -> add_to_tracked status filename
| "A " -> add_to_staged status filename
| "AM" -> add_to_staged_and_tracked status filename
| "AD" -> add_to_staged_and_tracked status filename
| " D" -> add_to_tracked status filename
| "D " -> add_to_staged status filename
| " R" -> add_to_tracked status filename
| "R " -> add_to_staged status filename
| "RM" -> add_to_staged_and_tracked status filename
| "RD" -> add_to_staged_and_tracked status filename
| " C" -> add_to_tracked status filename
| "C " -> add_to_staged status filename
| "CM" -> add_to_staged_and_tracked status filename
| "CD" -> add_to_staged_and_tracked status filename
| "DR" -> add_to_staged_and_tracked status filename
| "DC" -> add_to_staged_and_tracked status filename
| "DD" -> add_to_staged_and_tracked status filename
| "AU" -> add_to_staged_and_tracked status filename
| "UD" -> add_to_staged_and_tracked status filename
| "UA" -> add_to_staged_and_tracked status filename
| "DU" -> add_to_staged_and_tracked status filename
| "AA" -> add_to_staged_and_tracked status filename
| "UU" -> add_to_staged_and_tracked status filename
| _ -> status
(** [init d] initializes a git repository in the current working
directory if [n] is [None], otherwise initializes a git repository
with name [n] as a subdirectory in the current working directory *)
let init (dir : string option) : unit =
match dir with
| None -> ignore (P.init [||])
| Some d -> ignore (P.init [| d |])
(**[status_t_of_string_list lines] converts [lines] to a status*)
let status_t_of_string_list lines =
List.fold_left add_to_status_t empty_status_t lines
(**[status] gets the current git status*)
let status () =
let status = P.status [| "--porcelain" |] in
let lines = P.get_out status in
status_t_of_string_list lines
(**[checkout branch] switches to branch [branch]*)
let checkout branch =
let res = P.checkout [| branch |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(**[create_branch branch] creates a new branch [branch]*)
let create_branch branch =
let res = P.checkout [| "-b"; branch |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(**[delete_branch branch] deletes branch [branch]*)
let delete_branch branch =
let res = P.branch [| "-d"; branch |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(** [reset_hard c] puts the head at the commit with hash [c], deleting
any changes made after [c]. *)
let reset_hard commit =
let res = P.reset [| "--hard"; commit |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(** [reset_soft c] puts the head at the commit with has [c], but keeps
all changes made after [c]. *)
let reset_soft commit =
let res = P.reset [| "--soft"; commit |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(** [stash_apply] applies changes to the current working tree, leaving
them on the stash stack. *)
let stash_apply () =
let res = P.stash [| "apply" |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(** [stash_pop] applies changes to the current working tree and
removes them from the stash stack. *)
let stash_pop () =
let res = P.stash [| "pop" |] in
P.get_out res |> List.fold_left (fun acc x -> acc ^ x ^ "\n") ""
(**[get_untracked status] returns a list of all untracked files in
[status]*)
let get_untracked status = status.untracked
(**[get_tracked status] returns a list of all tracked files in
[status]*)
let get_tracked status = status.tracked
(**[get_staged status] returns a list of all staged files in [status]*)
let get_staged status = status.staged
(**[string_of_commit_t c] converts the tree and message of [c] to a
single string separated by a space*)
let string_of_commit_t c = c.tree ^ " " ^ c.msg
end