package stog_server_multi

  1. Overview
  2. Docs

Source file git_server.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
(*********************************************************************************)
(*                Stog                                                           *)
(*                                                                               *)
(*    Copyright (C) 2012-2015 INRIA All rights reserved.                         *)
(*    Author: Maxence Guesdon, INRIA Saclay                                      *)
(*                                                                               *)
(*    This program is free software; you can redistribute it and/or modify       *)
(*    it under the terms of the GNU General Public License as                    *)
(*    published by the Free Software Foundation, version 3 of the License.       *)
(*                                                                               *)
(*    This program is distributed in the hope that it will be useful,            *)
(*    but WITHOUT ANY WARRANTY; without even the implied warranty of             *)
(*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the               *)
(*    GNU General Public License for more details.                               *)
(*                                                                               *)
(*    You should have received a copy of the GNU General Public                  *)
(*    License along with this program; if not, write to the Free Software        *)
(*    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA                   *)
(*    02111-1307  USA                                                            *)
(*                                                                               *)
(*    As a special exception, you have permission to link this program           *)
(*    with the OCaml compiler and distribute executables, as long as you         *)
(*    follow the requirements of the GNU GPL in regard to all of the             *)
(*    software in the executable aside from the OCaml compiler.                  *)
(*                                                                               *)
(*    Contact: Maxence.Guesdon@inria.fr                                          *)
(*                                                                               *)
(*********************************************************************************)

(** *)

open Ojs_server
open Lwt.Infix
module SMap = Ojs_server.Server.SMap

type git_repo = {
  repo_dir : Ojs_base.Path.t ;
  origin_url : string ;
  origin_branch : string ;
  edit_branch : string ;
  } [@@deriving yojson]

let git_repo ~origin_url ~origin_branch ~edit_branch ~dir =
  { repo_dir = dir ; origin_url ; origin_branch ; edit_branch }


(*c==v=[Misc.try_finalize]=1.0====*)
let try_finalize f x finally y =
  let res =
    try f x
    with exn -> finally y; raise exn
  in
  finally y;
  res
(*/c==v=[Misc.try_finalize]=1.0====*)

let remove_file file = try Sys.remove file with _ -> ()

let run ?(merge_outputs=false) com =
  let stdout = Filename.temp_file "stogserver" ".stdout" in
  let stderr = Filename.temp_file "stogserver" ".stderr" in
  let command = Printf.sprintf "( %s ) %s"
    com
      (match merge_outputs with
       | false -> Printf.sprintf "> %s 2> %s" (Filename.quote stdout) (Filename.quote stderr)
       | true ->  Printf.sprintf "> %s 2>&1" (Filename.quote stdout)
      )
  in
  let result = Sys.command command in
  let s_out = Stog_base.Misc.string_of_file stdout in
  let s_err = if merge_outputs then "" else Stog_base.Misc.string_of_file stderr in
  remove_file stdout ; remove_file stderr ;
  match result with
    0 -> `Ok (s_out, s_err)
  | _ -> `Error (s_out, s_err)

let in_git_repo git ?merge_outputs com =
  let command = Printf.sprintf "cd %s && %s"
    (Filename.quote (Ojs_base.Path.to_string git.repo_dir)) com
  in
  match run ?merge_outputs command with
  | `Error (out,err)-> `Error (com, out, err)
  | (`Ok _) as x -> x

let with_sshkey key_file com =
  let sub = Filename.quote
    (Printf.sprintf "ssh-add %s; %s" (Filename.quote key_file) com)
  in
  Printf.sprintf "ssh-agent bash -c %s" sub

let with_sshkey_opt sshkey com =
  match sshkey with
  | None -> com
  | Some key_file -> with_sshkey key_file com

let clone ?sshkey git =
  let command = Printf.sprintf "git clone %s %s"
    (Filename.quote git.origin_url) (Filename.quote (Ojs_base.Path.to_string git.repo_dir))
  in
  let com = with_sshkey_opt sshkey command in
  match%lwt Lwt_preemptive.detach run com with
  | `Error (_,err) -> Lwt.fail_with (Printf.sprintf "Error while cloning %s:\n%s" git.origin_url err)
  | `Ok _ -> Lwt.return_unit

let set_user_info git ~name ~email =
  let com =
    Printf.sprintf "git config user.name %s && git config user.email %s"
      (Filename.quote name) (Filename.quote email)
  in
  match in_git_repo git com with
  | `Ok _ -> ()
  | `Error (_,_,err) -> failwith ("Command failed: "^com)

let current_branch git =
  let com = "git rev-parse --abbrev-ref HEAD" in
  match in_git_repo git com with
  | `Error (com,_,err) -> failwith (com^"\n"^err)
  | `Ok (s,_) ->
      match Stog_base.Misc.split_string s ['\n' ; '\r'] with
        [] -> failwith ("No current branch returned by "^com)
      | line :: _ -> line

let create_edit_branch git =
  let com = Printf.sprintf "git checkout -b %s" (Filename.quote git.edit_branch) in
  match in_git_repo git com with
  | `Error (_,_,err) -> failwith (com^"\n"^err)
  | `Ok _ -> ()

let status git =
  let git_com = "git status --porcelain -z" in
  match in_git_repo git git_com with
  | `Error (_,msg,_) -> failwith (git_com^"\n"^msg)
  | `Ok (str,_) -> Stog_multi_ed_common.Git_status.parse str

let has_local_changes git =
  let st = status git in
  let pred (s1, s2, _, _) =
    match s1, s2 with
    | `Q, _ | _, `Q
    | `I, _ | _, `I -> false
    | _ -> true
  in
  List.filter pred st <> []

let in_rebase git =
  let git_com = "ls `git rev-parse --git-dir` | grep rebase" in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error (_,_,_) -> false
  | `Ok (_,_) -> true

let pull_in_origin ?sshkey git =
  let ob = git.origin_branch in
  let b = git.edit_branch in
  let pull_origin () =
    let ob = Filename.quote ob in
    let git_com = Printf.sprintf
      "(git checkout %s && git pull origin %s)" ob ob
    in
    let ssh_com = with_sshkey_opt sshkey git_com in
    match in_git_repo git ~merge_outputs: true ssh_com with
    | `Error (com,msg,_) -> failwith (com^"\n"^msg)
    | `Ok _ -> ()
  in
  let checkout_edit_branch () =
    let git_com = Printf.sprintf "git checkout %s" (Filename.quote b) in
    match in_git_repo git ~merge_outputs: true git_com with
    | `Error (com,msg,_) -> failwith (com^"\n"^msg)
    | `Ok _ -> ()
  in
  try_finalize pull_origin () checkout_edit_branch ()

let diff_fetch_orig git =
  let git_com = "git diff  --name-only FETCH_HEAD..ORIG_HEAD" in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error (com,msg,_) -> failwith (git_com^"\n"^msg)
  | `Ok (msg,_) ->
      let lines = Stog_base.Misc.split_string msg ['\n' ; '\r'] in
      List.map Ojs_base.Path.of_string lines

let rebase_from_origin ?sshkey git =
  let st = status git in
  match in_rebase git with
  | false ->
      begin
        pull_in_origin ?sshkey git ;
        let git_com = Printf.sprintf "git rebase %s" (Filename.quote git.origin_branch) in
        match in_git_repo git ~merge_outputs: true git_com with
          `Error (com,msg,_) -> failwith (com^"\n"^msg)
        | `Ok (msg,_) -> (msg, diff_fetch_orig git)
      end
  | true ->
      match List.exists Stog_multi_ed_common.Git_status.is_unmerged st || has_local_changes git with
      | true ->
          begin
            let git_com = "git rebase --continue" in
            match in_git_repo git ~merge_outputs: true git_com with
              `Error (com,msg,_) -> failwith (com^"\n"^msg)
            | `Ok (msg,_) -> (msg, diff_fetch_orig git)
          end
      | false ->
          begin
            let git_com = "git rebase --skip" in
            match in_git_repo git ~merge_outputs: true git_com with
              `Error (com,msg,_) -> failwith (com^"\n"^msg)
            | `Ok ("",_) -> ("Rebase skipped", [])
            | `Ok (msg,_) -> (msg, [])
          end

let differs_from_origin git =
  let git_com = Printf.sprintf "git diff %s" (Filename.quote git.origin_branch) in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error _ -> false
  | `Ok (msg,_) -> Stog_base.Misc.strip_string msg <> ""

let merge_in_origin git =
  if has_local_changes git then
    failwith "There are local changes. Please commit first.";
  let git_com = Printf.sprintf
    "git checkout %s && git merge %s && git checkout %s"
      (Filename.quote git.origin_branch)
      (Filename.quote git.edit_branch)
      (Filename.quote git.edit_branch)
  in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error (_,msg,_) -> failwith (git_com^"\n"^msg)
  | `Ok (msg,_) -> msg

let push_origin ?sshkey git =
  let com = Printf.sprintf
    "git checkout %s && git push origin %s && git checkout %s"
      (Filename.quote git.origin_branch)
      (Filename.quote git.origin_branch)
      (Filename.quote git.edit_branch)
  in
  let git_com = with_sshkey_opt sshkey com in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error (_,msg,_) -> failwith (com^"\n"^msg)
  | `Ok (msg,_) -> msg

let push ?sshkey git =
  if has_local_changes git then
    failwith "There are local changes. Please commit first.";
  let st = status git in
  if List.exists Stog_multi_ed_common.Git_status.is_unmerged st || in_rebase git then
    failwith "You're currently merge files. Please finish merging first (use commit and pull)." ;
  let (rebase_msg, modified_files) = rebase_from_origin ?sshkey git in
  let merge_msg = merge_in_origin git in
  try
    let push_msg = push_origin ?sshkey git in
    Printf.sprintf "%s\n%s\n%s" rebase_msg merge_msg push_msg
  with Failure msg ->
      failwith (Printf.sprintf "%s\n\n%s\n\n%s" rebase_msg merge_msg msg)

let commit git paths msg =
  let st = status git in
  let git_com =
    match in_rebase git with
    | false ->
        let (add, args) =
          match paths with
            [] -> (false, "-a")
          | _ ->
              (true,
               String.concat " "
                 (List.map (fun p -> Filename.quote (Ojs_base.Path.to_string p)) paths)
              )
        in
        Printf.sprintf "%sgit commit -m%s %s"
          (if add then Printf.sprintf "git add %s &&" args else "")
          (Filename.quote msg) args
    | true ->
        match List.filter Stog_multi_ed_common.Git_status.is_unmerged st with
        | [] -> "echo 'Nothing to be done'"
        | unmerged ->
            begin
              let args =
                String.concat " "
                  (List.map (fun (_,_,p,_) -> Filename.quote (Ojs_base.Path.to_string p))
                   unmerged)
              in
              Printf.sprintf "git add %s" args
            end
  in
  match in_git_repo git ~merge_outputs: true git_com with
    `Error (_,msg,_) -> failwith (git_com^"\n"^msg)
  | `Ok("",_) -> "Done"
  | `Ok (msg,_) -> msg

module Make (P: Stog_multi_ed_common.Git.P) =
  struct
    class repo
      (broadcall : P.server_msg -> (P.client_msg -> unit Lwt.t) -> unit Lwt.t)
        (broadcast : P.server_msg -> unit Lwt.t) ~id ?sshkey git =
    object(self)
      method id = (id : string)
      method git = (git : git_repo)
      method sshkey = (sshkey : string option)
      val git_mutex = Lwt_mutex.create ()

      method do_git :
         'a . git_repo -> (git_repo -> 'a Lwt.t) -> 'a Lwt.t =
         fun git f ->
         Lwt_mutex.with_lock git_mutex (fun () -> f git)

      method git_action reply_msg f =
         self#do_git git
           (fun _ ->
              Lwt.catch
                (fun () ->
                  Lwt_preemptive.detach f () >>= fun msg ->
                    reply_msg (P.SOk msg))
                (function
                 | Failure msg -> reply_msg (P.SError msg)
                 | e -> reply_msg (P.SError (Printexc.to_string e))
                 )
            )

      method handle_get_status reply_msg =
        let%lwt st = self#do_git git (Lwt_preemptive.detach status) in
        reply_msg (P.SStatus st)

      method handle_commit reply_msg paths msg =
        self#git_action reply_msg (fun () -> commit git paths msg)

      method handle_rebase_from_origin reply_msg =
        self#git_action reply_msg
              (fun () ->
                 let (msg, files) = rebase_from_origin ?sshkey git in
                 let msg_f = match files with
                     [] -> ""
                   | _ ->
                     Printf.sprintf "\nThe following files were modified:\n- %s"
                       (String.concat "\n -" (List.map Ojs_base.Path.to_string files))
                 in
                 Printf.sprintf "%s%s" msg msg_f
              )


      method handle_push reply_msg =
        self#git_action reply_msg (fun () -> push ?sshkey git)

      method handle_message
            (send_msg : P.server_msg -> unit Lwt.t) (msg : P.client_msg) =
        self#handle_call send_msg msg

      method handle_call
            (reply_msg : P.server_msg -> unit Lwt.t) (msg : P.client_msg) =
        match msg with
        | P.Status ->
            self#handle_get_status reply_msg
        | P.Commit (paths, msg) ->
            self#handle_commit reply_msg paths msg
        | P.Rebase_from_origin ->
            self#handle_rebase_from_origin reply_msg
        | P.Push ->
            self#handle_push reply_msg
        | _ ->
            reply_msg (P.SError "Unhandled message in git repo")
      end

class repos
  (broadcall : P.app_server_msg -> (P.app_client_msg -> unit Lwt.t) -> unit Lwt.t)
    (broadcast : P.app_server_msg -> unit Lwt.t)
    (spawn : (P.server_msg -> (P.client_msg -> unit Lwt.t) -> unit Lwt.t) ->
     (P.server_msg -> unit Lwt.t) ->
       id: string -> ?sshkey: string -> git_repo -> repo
    )
    =
    object(self)
      val mutable repos = (SMap.empty : repo SMap.t)

      method repo id =
        try SMap.find id repos
        with Not_found -> failwith (Printf.sprintf "No repository with id %S" id)

      method add_repo ~id ?sshkey git =
        let broadcall msg cb =
          let cb msg =
             match P.unpack_client_msg msg with
             | Some (_, msg) -> cb msg
             | None -> Lwt.return_unit
          in
          broadcall (P.pack_server_msg id msg) cb
        in
        let broadcast msg = broadcast (P.pack_server_msg id msg) in
        let repo = spawn broadcall broadcast ~id ?sshkey git in
        repos <- SMap.add id repo repos;
        repo

      method handle_message
        (send_msg : P.app_server_msg -> unit Lwt.t) (msg : P.app_client_msg) =
          match P.unpack_client_msg msg with
          | Some (id, msg) ->
              let send_msg msg = send_msg (P.pack_server_msg id msg) in
              (self#repo id)#handle_message send_msg msg
          | None -> Lwt.return_unit

      method handle_call
         (return : P.app_server_msg -> unit Lwt.t) (msg : P.app_client_msg) =
        match P.unpack_client_msg msg with
        | Some (id, msg) ->
            let reply_msg msg = return (P.pack_server_msg id msg) in
            (self#repo id)#handle_call reply_msg msg
        | None -> Lwt.return_unit
  end
end
OCaml

Innovation. Community. Security.