├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── changelog.yml ├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── dune ├── dune-project ├── merge-fmt-help.txt ├── merge-fmt-mergetool-help.txt ├── merge-fmt-setup-merge-help.txt ├── merge-fmt-setup-mergetool-help.txt ├── merge-fmt.opam ├── src ├── common.ml ├── dune ├── fmters.ml ├── fmters.mli ├── merge_cmd.ml ├── merge_cmd.mli ├── merge_fmt.ml ├── merge_fmt.mli ├── resolve_cmd.ml ├── resolve_cmd.mli ├── setup_cmd.ml └── setup_cmd.mli └── test ├── common.ml ├── dune ├── merge.ml ├── merge_dune.ml ├── mergetool.ml ├── partial.ml ├── rebase.diff ├── rebase_a.ml ├── rebase_b.ml ├── resolve1.ml └── resolve2.ml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | schedule: 9 | # Prime the caches every Monday 10 | - cron: 0 1 * * MON 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | ocaml-compiler: 20 | - "5.3" 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | 28 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 29 | uses: ocaml/setup-ocaml@v3 30 | with: 31 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 32 | 33 | - run: opam install . --with-test 34 | 35 | - run: opam exec -- make all 36 | 37 | - run: opam exec -- make test 38 | 39 | - run: opam exec -- git diff --exit-code 40 | 41 | lint-fmt: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - name: Use OCaml 5.x 48 | uses: ocaml/setup-ocaml@v3 49 | with: 50 | ocaml-compiler: "5.2" 51 | 52 | - uses: ocaml/setup-ocaml/lint-fmt@v3 53 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Check changelog 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | types: 8 | - labeled 9 | - opened 10 | - reopened 11 | - synchronize 12 | - unlabeled 13 | 14 | jobs: 15 | check-changelog: 16 | name: Check changelog 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check changelog 20 | uses: tarides/changelog-check-action@v1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Boring file regexps: 2 | 3 | # C object files 4 | *.o 5 | *.a 6 | *.so 7 | # Ocaml object files 8 | *.cmi 9 | *.cmo 10 | *.cmx 11 | *.cma 12 | *.cmxa 13 | *.cmxs 14 | *.cmjs 15 | 16 | # generated by dune 17 | *.merlin 18 | *.install 19 | 20 | # backup files 21 | *~ 22 | *# 23 | .#* 24 | 25 | _opam 26 | _build -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | profile=conventional 2 | type-decl=sparse 3 | break-separators=before 4 | if-then-else=keyword-first 5 | dock-collection-brackets=false 6 | version=0.27.0 -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.3 (2023-02-18) 2 | 3 | - Add support of dune formatter. (#3) 4 | 5 | ## 0.2 (2019-02-19) 6 | 7 | - Initial release. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | dune build @default 4 | $(MAKE) help 5 | 6 | help: 7 | dune build \ 8 | merge-fmt-help.txt \ 9 | merge-fmt-mergetool-help.txt \ 10 | merge-fmt-setup-mergetool-help.txt \ 11 | merge-fmt-setup-merge-help.txt 12 | .PHONY: test 13 | test: 14 | dune build @runtest 15 | 16 | .PHONY: clean 17 | clean: 18 | dune clean 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Merge-fmt - git mergetool leveraging code formatters 2 | ==================================================== 3 | 4 | WARNING: This tool is still experimental. 5 | 6 | `merge-fmt` is a small wrapper on top git commands to help resolve conflicts by leveraging 7 | code formatters. 8 | 9 | `merge-fmt` currently only knows about the following formatters: 10 | - [ocamlformat](https://github.com/ocaml-ppx/ocamlformat) for OCaml. 11 | - [refmt](https://github.com/facebook/reason) for reason. 12 | - [dune](https://github.com/ocaml/dune) for dune. 13 | 14 | Note that supporting new code formatters is trivial. 15 | 16 | Getting started 17 | ---------------- 18 | There are three ways to use merge-fmt. 19 | 20 | ### Standalone 21 | Just call `merge-fmt` while there are unresolved conflicts. `merge-fmt` will try 22 | resolve conflicts automatically. 23 | 24 | ### As a Git mergetool 25 | `merge-fmt` can act as a git [mergetool](https://git-scm.com/docs/git-mergetool). 26 | First configure the current git repository with 27 | ``` 28 | merge-fmt setup-mergetool 29 | git config --local mergetool.mergefmt.cmd 'merge-fmt mergetool --base=$BASE --current=$LOCAL --other=$REMOTE -o $MERGED' 30 | git config --local mergetool.mergefmt.trustExitCode true 31 | ``` 32 | Then, use `git mergetool` to resolve conflicts with 33 | ```git mergetool -t mergefmt``` 34 | 35 | ### As a git merge driver 36 | `merge-fmt` can act as a git [merge driver](https://git-scm.com/docs/gitattributes). 37 | Configure the current git repository to use merge-fmt as the default merge driver. 38 | ``` 39 | $ merge-fmt setup-merge 40 | git config --local merge.mergefmt.name 'merge-fmt driver' 41 | git config --local merge.mergefmt.driver 'merge-fmt mergetool --base=%O --current=%A --other=%B -o %A --name=%P' 42 | git config --local merge.tool 'mergefmt' 43 | git config --local merge.default 'mergefmt' 44 | ``` 45 | 46 | 47 | Install 48 | ------- 49 | ```sh 50 | $ opam pin add merge-fmt git@github.com:hhugo/merge-fmt.git 51 | ``` 52 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets merge-fmt-help.txt) 3 | (deps ./src/merge_fmt.exe) 4 | (mode promote) 5 | (action 6 | (with-stdout-to 7 | %{targets} 8 | (run ./src/merge_fmt.exe --help=plain)))) 9 | 10 | (rule 11 | (targets merge-fmt-mergetool-help.txt) 12 | (deps ./src/merge_fmt.exe) 13 | (mode promote) 14 | (action 15 | (with-stdout-to 16 | %{targets} 17 | (run ./src/merge_fmt.exe mergetool --help=plain)))) 18 | 19 | (rule 20 | (targets merge-fmt-setup-mergetool-help.txt) 21 | (deps ./src/merge_fmt.exe) 22 | (mode promote) 23 | (action 24 | (with-stdout-to 25 | %{targets} 26 | (run ./src/merge_fmt.exe setup-mergetool --help=plain)))) 27 | 28 | (rule 29 | (targets merge-fmt-setup-merge-help.txt) 30 | (deps ./src/merge_fmt.exe) 31 | (mode promote) 32 | (action 33 | (with-stdout-to 34 | %{targets} 35 | (run ./src/merge_fmt.exe setup-merge --help=plain)))) 36 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | 3 | (name merge-fmt) 4 | 5 | (generate_opam_files true) 6 | 7 | 8 | 9 | (authors "Hugo Heuzard") 10 | 11 | (maintainers "hugo.heuzard@gmail.com") 12 | (source (github hhugo/merge-fmt)) 13 | 14 | (documentation "https://hhugo.github.io/merge-fmt/") 15 | 16 | (license "MIT") 17 | 18 | (package 19 | (name merge-fmt) 20 | (synopsis "Git mergetool leveraging code formatters") 21 | (description 22 | "`merge-fmt` is a small wrapper on top git commands to help resolve conflicts by leveraging code formatters.") 23 | (depends 24 | (ocaml 25 | (>= 4.8)) 26 | (cmdliner 27 | (>= 1.1.0)) 28 | base 29 | stdio 30 | (ppx_expect :with-test) 31 | (core_unix :with-test) 32 | (ocamlformat 33 | (and 34 | (= 0.27.0) 35 | :with-test)))) 36 | -------------------------------------------------------------------------------- /merge-fmt-help.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | merge-fmt - Try to automatically resolve conflicts due to code 3 | formatting 4 | 5 | SYNOPSIS 6 | merge-fmt [COMMAND] … 7 | 8 | COMMANDS 9 | mergetool [OPTION]… 10 | git mergetool 11 | 12 | setup-merge [--echo] [--merge-fmt-path=VAL] [--update] [OPTION]… 13 | Register the [merge-fmt] mergetool as the default merge driver in 14 | git 15 | 16 | setup-mergetool [--echo] [--merge-fmt-path=VAL] [--update] [OPTION]… 17 | Register the [merge-fmt] mergetool in git 18 | 19 | OPTIONS 20 | --dune=VAL 21 | dune path 22 | 23 | --echo 24 | Echo all commands. 25 | 26 | --ocamlformat=VAL 27 | ocamlformat path 28 | 29 | --refmt=VAL 30 | refmt path 31 | 32 | COMMON OPTIONS 33 | --help[=FMT] (default=auto) 34 | Show this help in format FMT. The value FMT must be one of auto, 35 | pager, groff or plain. With auto, the format is pager or plain 36 | whenever the TERM env var is dumb or undefined. 37 | 38 | EXIT STATUS 39 | merge-fmt exits with: 40 | 41 | 0 on success. 42 | 43 | 123 on indiscriminate errors reported on standard error. 44 | 45 | 124 on command line parsing errors. 46 | 47 | 125 on unexpected internal errors (bugs). 48 | 49 | -------------------------------------------------------------------------------- /merge-fmt-mergetool-help.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | merge-fmt-mergetool - git mergetool 3 | 4 | SYNOPSIS 5 | merge-fmt mergetool [OPTION]… 6 | 7 | OPTIONS 8 | --base= 9 | 10 | --current= 11 | 12 | --dune=VAL 13 | dune path 14 | 15 | --echo 16 | Echo all commands. 17 | 18 | --name= 19 | pathname in which the merged result will be stored 20 | 21 | -o 22 | 23 | --ocamlformat=VAL 24 | ocamlformat path 25 | 26 | --other= 27 | 28 | --refmt=VAL 29 | refmt path 30 | 31 | COMMON OPTIONS 32 | --help[=FMT] (default=auto) 33 | Show this help in format FMT. The value FMT must be one of auto, 34 | pager, groff or plain. With auto, the format is pager or plain 35 | whenever the TERM env var is dumb or undefined. 36 | 37 | EXIT STATUS 38 | merge-fmt mergetool exits with: 39 | 40 | 0 on success. 41 | 42 | 123 on indiscriminate errors reported on standard error. 43 | 44 | 124 on command line parsing errors. 45 | 46 | 125 on unexpected internal errors (bugs). 47 | 48 | SEE ALSO 49 | merge-fmt(1) 50 | 51 | -------------------------------------------------------------------------------- /merge-fmt-setup-merge-help.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | merge-fmt-setup-merge - Register the [merge-fmt] mergetool as the 3 | default merge driver in git 4 | 5 | SYNOPSIS 6 | merge-fmt setup-merge [--echo] [--merge-fmt-path=VAL] [--update] 7 | [OPTION]… 8 | 9 | OPTIONS 10 | --echo 11 | Echo all commands. 12 | 13 | --merge-fmt-path=VAL 14 | Path of merge-fmt. 15 | 16 | --update 17 | Update the git config of the current repository. Just output 18 | commands otherwise. 19 | 20 | COMMON OPTIONS 21 | --help[=FMT] (default=auto) 22 | Show this help in format FMT. The value FMT must be one of auto, 23 | pager, groff or plain. With auto, the format is pager or plain 24 | whenever the TERM env var is dumb or undefined. 25 | 26 | EXIT STATUS 27 | merge-fmt setup-merge exits with: 28 | 29 | 0 on success. 30 | 31 | 123 on indiscriminate errors reported on standard error. 32 | 33 | 124 on command line parsing errors. 34 | 35 | 125 on unexpected internal errors (bugs). 36 | 37 | SEE ALSO 38 | merge-fmt(1) 39 | 40 | -------------------------------------------------------------------------------- /merge-fmt-setup-mergetool-help.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | merge-fmt-setup-mergetool - Register the [merge-fmt] mergetool in git 3 | 4 | SYNOPSIS 5 | merge-fmt setup-mergetool [--echo] [--merge-fmt-path=VAL] [--update] 6 | [OPTION]… 7 | 8 | OPTIONS 9 | --echo 10 | Echo all commands. 11 | 12 | --merge-fmt-path=VAL 13 | Path of merge-fmt. 14 | 15 | --update 16 | Update the git config of the current repository. Just output 17 | commands otherwise. 18 | 19 | COMMON OPTIONS 20 | --help[=FMT] (default=auto) 21 | Show this help in format FMT. The value FMT must be one of auto, 22 | pager, groff or plain. With auto, the format is pager or plain 23 | whenever the TERM env var is dumb or undefined. 24 | 25 | EXIT STATUS 26 | merge-fmt setup-mergetool exits with: 27 | 28 | 0 on success. 29 | 30 | 123 on indiscriminate errors reported on standard error. 31 | 32 | 124 on command line parsing errors. 33 | 34 | 125 on unexpected internal errors (bugs). 35 | 36 | SEE ALSO 37 | merge-fmt(1) 38 | 39 | -------------------------------------------------------------------------------- /merge-fmt.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Git mergetool leveraging code formatters" 4 | description: 5 | "`merge-fmt` is a small wrapper on top git commands to help resolve conflicts by leveraging code formatters." 6 | maintainer: ["hugo.heuzard@gmail.com"] 7 | authors: ["Hugo Heuzard"] 8 | license: "MIT" 9 | homepage: "https://github.com/hhugo/merge-fmt" 10 | doc: "https://hhugo.github.io/merge-fmt/" 11 | bug-reports: "https://github.com/hhugo/merge-fmt/issues" 12 | depends: [ 13 | "dune" {>= "3.0"} 14 | "ocaml" {>= "4.8"} 15 | "cmdliner" {>= "1.1.0"} 16 | "base" 17 | "stdio" 18 | "ppx_expect" {with-test} 19 | "core_unix" {with-test} 20 | "ocamlformat" {= "0.27.0" & with-test} 21 | "odoc" {with-doc} 22 | ] 23 | build: [ 24 | ["dune" "subst"] {dev} 25 | [ 26 | "dune" 27 | "build" 28 | "-p" 29 | name 30 | "-j" 31 | jobs 32 | "@install" 33 | "@runtest" {with-test} 34 | "@doc" {with-doc} 35 | ] 36 | ] 37 | dev-repo: "git+https://github.com/hhugo/merge-fmt.git" 38 | -------------------------------------------------------------------------------- /src/common.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Stdio 3 | 4 | let sprintf = Printf.sprintf 5 | 6 | let open_process_in ~echo fmt = 7 | Printf.ksprintf 8 | (fun s -> 9 | if echo then eprintf "+ %s\n%!" s; 10 | Unix.open_process_in s) 11 | fmt 12 | 13 | let open_process_in_respect_exit ~echo fmt = 14 | Printf.ksprintf 15 | (fun s -> 16 | if echo then eprintf "+ %s\n%!" s; 17 | let ic = Unix.open_process_in s in 18 | let contents = In_channel.input_all ic in 19 | match Unix.close_process_in ic with 20 | | WEXITED 0 -> contents 21 | | WEXITED n -> Stdlib.exit n 22 | | WSIGNALED _ | WSTOPPED _ -> Stdlib.exit 1) 23 | fmt 24 | 25 | let system ~echo fmt = 26 | Printf.ksprintf 27 | (fun s -> 28 | if echo then eprintf "+ %s\n%!" s; 29 | match Unix.system s with WEXITED 0 -> Ok () | _ -> Error ()) 30 | fmt 31 | 32 | let system_respect_exit ~echo fmt = 33 | Printf.ksprintf 34 | (fun s -> 35 | if echo then eprintf "+ %s\n%!" s; 36 | match Unix.system s with 37 | | WEXITED 0 -> () 38 | | WEXITED n -> Stdlib.exit n 39 | | WSIGNALED _ | WSTOPPED _ -> Stdlib.exit 1) 40 | fmt 41 | 42 | module Flags = struct 43 | open Cmdliner 44 | 45 | let echo = 46 | let doc = "Echo all commands." in 47 | Arg.(value & flag & info [ "echo" ] ~doc) 48 | end 49 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names merge_fmt) 3 | (public_names merge-fmt) 4 | (libraries base stdio unix cmdliner)) 5 | -------------------------------------------------------------------------------- /src/fmters.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Common 3 | 4 | type config = 5 | { ocamlformat_path : string option 6 | ; refmt_path : string option 7 | ; dune_path : string option 8 | } 9 | 10 | type t = 11 | | Inplace of string 12 | | Stdout of string 13 | 14 | let transfer ic oc = 15 | let b = Bytes.create 4096 in 16 | let rec loop () = 17 | match Stdlib.input ic b 0 (Bytes.length b) with 18 | | 0 -> () 19 | | l -> 20 | Stdlib.output oc b 0 l; 21 | loop () 22 | in 23 | loop () 24 | 25 | let ocamlformat ~bin ~name = 26 | Inplace 27 | (sprintf "%s -i %s" 28 | (Option.value ~default:"ocamlformat" bin) 29 | (Option.value_map ~default:"" ~f:(fun name -> " --name=" ^ name) name)) 30 | 31 | let refmt ~bin = 32 | Inplace (sprintf "%s --inplace" (Option.value ~default:"refmt" bin)) 33 | 34 | let dune ~bin = 35 | Stdout (sprintf "%s format-dune-file --" (Option.value ~default:"dune" bin)) 36 | 37 | let find ~config ~filename ~name = 38 | let filename = Option.value ~default:filename name in 39 | match (filename, Stdlib.Filename.extension filename, config) with 40 | | _, (".ml" | ".mli"), { ocamlformat_path; _ } -> 41 | Some (ocamlformat ~bin:ocamlformat_path ~name) 42 | | _, (".re" | ".rei"), { refmt_path; _ } -> Some (refmt ~bin:refmt_path) 43 | | ("dune" | "dune-project" | "dune-workspace"), "", { dune_path; _ } -> 44 | Some (dune ~bin:dune_path) 45 | | _ -> None 46 | 47 | let run t ~echo ~filename = 48 | match t with 49 | | Inplace t -> system ~echo "%s %s" t filename 50 | | Stdout t -> ( 51 | let ic = open_process_in ~echo "%s %s" t filename in 52 | let tmp_file, oc = Stdlib.Filename.open_temp_file "merge-fmt" "stdout" in 53 | transfer ic oc; 54 | Stdlib.close_out oc; 55 | match Unix.close_process_in ic with 56 | | WEXITED 0 -> 57 | Stdlib.Sys.rename tmp_file filename; 58 | Ok () 59 | | WEXITED n -> 60 | Stdlib.Printf.eprintf ">>> Exit with %d\n" n; 61 | Error () 62 | | WSIGNALED _ | WSTOPPED _ -> Error ()) 63 | 64 | module Flags = struct 65 | open Cmdliner 66 | 67 | let ocamlformat_path = 68 | let doc = "ocamlformat path" in 69 | Arg.(value & opt (some string) None & info [ "ocamlformat" ] ~doc) 70 | 71 | let refmt_path = 72 | let doc = "refmt path" in 73 | Arg.(value & opt (some string) None & info [ "refmt" ] ~doc) 74 | 75 | let dune_path = 76 | let doc = "dune path" in 77 | Arg.(value & opt (some string) None & info [ "dune" ] ~doc) 78 | 79 | let t = 80 | Term.( 81 | const (fun ocamlformat_path refmt_path dune_path -> 82 | { ocamlformat_path; refmt_path; dune_path }) 83 | $ ocamlformat_path $ refmt_path $ dune_path) 84 | end 85 | -------------------------------------------------------------------------------- /src/fmters.mli: -------------------------------------------------------------------------------- 1 | open Base 2 | 3 | type config 4 | type t 5 | 6 | val find : config:config -> filename:string -> name:string option -> t option 7 | val run : t -> echo:bool -> filename:string -> (unit, unit) Result.t 8 | 9 | module Flags : sig 10 | open Cmdliner 11 | 12 | val t : config Term.t 13 | end 14 | -------------------------------------------------------------------------------- /src/merge_cmd.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Stdio 3 | open Common 4 | 5 | let debug_oc = 6 | lazy 7 | (Out_channel.create ~append:true 8 | (Stdlib.Filename.concat 9 | (Stdlib.Filename.get_temp_dir_name ()) 10 | "merge-fmt.log")) 11 | 12 | let debug fmt = 13 | if true 14 | then Printf.ksprintf (fun _ -> ()) fmt 15 | else Printf.ksprintf (Out_channel.fprintf (Lazy.force debug_oc) "%s") fmt 16 | 17 | let merge config echo current base other output name = 18 | match (current, base, other) with 19 | | (None | Some ""), _, _ | _, (None | Some ""), _ | _, _, (None | Some "") -> 20 | Stdlib.exit 1 21 | | Some current, Some base, Some other -> ( 22 | match Fmters.find ~config ~filename:current ~name with 23 | | None -> 24 | debug "Couldn't find a formatter for %s\n%!" current; 25 | system_respect_exit ~echo "git merge-file %s %s %s" current base other 26 | | Some formatter -> ( 27 | let x = 28 | Fmters.run formatter ~echo ~filename:current 29 | |> Result.map_error ~f:(Fn.const "current") 30 | and y = 31 | Fmters.run formatter ~echo ~filename:other 32 | |> Result.map_error ~f:(Fn.const "other") 33 | and z = 34 | Fmters.run formatter ~echo ~filename:base 35 | |> Result.map_error ~f:(Fn.const "base") 36 | in 37 | match Result.combine_errors [ x; y; z ] with 38 | | Error _ -> Stdlib.exit 1 39 | | Ok (_ : unit list) -> 40 | debug "process all three revision successfully\n%!"; 41 | debug "running git merge-file\n%!"; 42 | let result = 43 | open_process_in_respect_exit ~echo "git merge-file -p %s %s %s" 44 | current base other 45 | in 46 | (match output with 47 | | None -> Out_channel.output_string stdout result 48 | | Some o -> Out_channel.write_all o ~data:result); 49 | Stdlib.exit 0)) 50 | 51 | open Cmdliner 52 | 53 | let cmd = 54 | let current = 55 | let doc = "" in 56 | Arg.( 57 | value 58 | & opt (some file) None 59 | & info [ "current" ] ~docv:"" ~doc) 60 | in 61 | let base = 62 | let doc = "" in 63 | Arg.( 64 | value & opt (some file) None & info [ "base" ] ~docv:"" ~doc) 65 | in 66 | let other = 67 | let doc = "" in 68 | Arg.( 69 | value & opt (some file) None & info [ "other" ] ~docv:"" ~doc) 70 | in 71 | let output = 72 | let doc = "" in 73 | Arg.(value & opt (some file) None & info [ "o" ] ~docv:"" ~doc) 74 | in 75 | let result_name = 76 | let doc = "pathname in which the merged result will be stored" in 77 | Arg.( 78 | value & opt (some file) None & info [ "name" ] ~docv:"" ~doc) 79 | in 80 | let doc = "git mergetool" in 81 | let term = 82 | Term.( 83 | const merge $ Fmters.Flags.t $ Flags.echo $ current $ base $ other 84 | $ output $ result_name) 85 | in 86 | Cmd.v (Cmd.info ~doc "mergetool") term 87 | -------------------------------------------------------------------------------- /src/merge_cmd.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Cmdliner 3 | 4 | val cmd : unit Cmd.t 5 | -------------------------------------------------------------------------------- /src/merge_fmt.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | open! Cmdliner 5 | 6 | let cmds = 7 | Cmd.group ~default:(fst Resolve_cmd.cmd) (snd Resolve_cmd.cmd) 8 | [ Merge_cmd.cmd; Setup_cmd.mergetool; Setup_cmd.merge ] 9 | 10 | let () = Stdlib.exit (Cmdliner.Cmd.eval cmds) 11 | -------------------------------------------------------------------------------- /src/merge_fmt.mli: -------------------------------------------------------------------------------- 1 | (* empty *) 2 | -------------------------------------------------------------------------------- /src/resolve_cmd.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Stdio 3 | open Common 4 | 5 | type version = 6 | | Common 7 | | Theirs 8 | | Ours 9 | 10 | let string_of_version = function 11 | | Common -> "common" 12 | | Theirs -> "theirs" 13 | | Ours -> "ours" 14 | 15 | type rev = Object of string 16 | 17 | type versions = 18 | { common : rev 19 | ; theirs : rev 20 | ; ours : rev 21 | } 22 | 23 | let conflict ~filename = 24 | In_channel.with_file filename ~f:(fun ic -> 25 | let rec loop n = 26 | match In_channel.input_line ic with 27 | | None -> n 28 | | Some line -> 29 | if String.is_prefix ~prefix:"<<<<<<<" line 30 | then loop (Int.succ n) 31 | else loop n 32 | in 33 | loop 0) 34 | 35 | let ls ~echo () = 36 | let ic = open_process_in ~echo "git ls-files -u" in 37 | let rec loop acc = 38 | match In_channel.input_line ic with 39 | | None -> acc 40 | | Some line -> ( 41 | match String.split_on_chars ~on:[ ' '; '\t' ] line with 42 | | [ _; id; num; file ] -> loop ((file, (Int.of_string num, id)) :: acc) 43 | | _ -> failwith "unexpected format") 44 | in 45 | let map = Map.of_alist_multi (module String) (loop []) in 46 | Map.map map ~f:(fun l -> 47 | let l = List.sort l ~compare:(Comparable.lift ~f:fst Int.compare) in 48 | match l with 49 | | [ (1, common); (2, ours); (3, theirs) ] -> 50 | Ok 51 | { common = Object common 52 | ; ours = Object ours 53 | ; theirs = Object theirs 54 | } 55 | | _ -> Error "not a 3-way merge") 56 | 57 | let show ~echo version versions = 58 | let obj = 59 | match version with 60 | | Ours -> versions.ours 61 | | Theirs -> versions.theirs 62 | | Common -> versions.common 63 | in 64 | match obj with 65 | | Object obj -> 66 | open_process_in ~echo "git show %s" obj |> In_channel.input_all 67 | 68 | let create_tmp ~echo fn version versions = 69 | let content = show ~echo version versions in 70 | let ext = Stdlib.Filename.extension fn in 71 | let base = 72 | if String.equal ext "" then fn else Stdlib.Filename.chop_extension fn 73 | in 74 | let fn' = sprintf "%s.%s%s" base (string_of_version version) ext in 75 | let oc = Out_channel.create fn' in 76 | Out_channel.output_string oc content; 77 | Out_channel.close oc; 78 | fn' 79 | 80 | let merge ~echo ~ours ~common ~theirs ~output = 81 | system ~echo "git merge-file -p %s %s %s > %s" ours common theirs output 82 | 83 | let git_add ~echo ~filename = system ~echo "git add %s" filename 84 | 85 | let fix ~echo ~filename ~versions ~formatter = 86 | let ours = create_tmp ~echo filename Ours versions in 87 | let theirs = create_tmp ~echo filename Theirs versions in 88 | let common = create_tmp ~echo filename Common versions in 89 | let x = 90 | Fmters.run formatter ~echo ~filename:ours 91 | |> Result.map_error ~f:(Fn.const ours) 92 | and y = 93 | Fmters.run formatter ~echo ~filename:theirs 94 | |> Result.map_error ~f:(Fn.const theirs) 95 | and z = 96 | Fmters.run formatter ~echo ~filename:common 97 | |> Result.map_error ~f:(Fn.const common) 98 | in 99 | match Result.combine_errors_unit [ x; y; z ] with 100 | | Error l -> 101 | eprintf "Failed to format %s\n%!" (String.concat ~sep:", " l); 102 | Error () 103 | | Ok () -> ( 104 | match merge ~echo ~ours ~theirs ~common ~output:filename with 105 | | Error _ -> Error () 106 | | Ok () -> 107 | Unix.unlink ours; 108 | Unix.unlink theirs; 109 | Unix.unlink common; 110 | Ok ()) 111 | 112 | let resolve config echo () = 113 | let all = ls ~echo () in 114 | if Map.is_empty all 115 | then ( 116 | eprintf "Nothing to resolve\n%!"; 117 | Stdlib.exit 1); 118 | Map.iteri all ~f:(fun ~key:filename ~data:versions -> 119 | match versions with 120 | | Ok versions -> ( 121 | match Fmters.find ~config ~filename ~name:None with 122 | | Some formatter -> 123 | let n1 = conflict ~filename in 124 | Result.bind (fix ~echo ~filename ~versions ~formatter) 125 | ~f:(fun () -> git_add ~echo ~filename) 126 | |> (ignore : (unit, unit) Result.t -> unit); 127 | let n2 = conflict ~filename in 128 | if n2 > n1 129 | then eprintf "Resolved ?? %s\n%!" filename 130 | else eprintf "Resolved %d/%d %s\n%!" (n1 - n2) n1 filename 131 | | None -> eprintf "Ignore %s (no formatter register)\n%!" filename) 132 | | Error reason -> eprintf "Ignore %s (%s)\n%!" filename reason); 133 | let all = ls ~echo () in 134 | if Map.is_empty all then Stdlib.exit 0 else Stdlib.exit 1 135 | 136 | open Cmdliner 137 | 138 | let cmd = 139 | let doc = "Try to automatically resolve conflicts due to code formatting" in 140 | ( Term.(const resolve $ Fmters.Flags.t $ Flags.echo $ const ()) 141 | , Cmd.info ~doc "merge-fmt" ) 142 | -------------------------------------------------------------------------------- /src/resolve_cmd.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Cmdliner 3 | 4 | val cmd : unit Term.t * Cmd.info 5 | -------------------------------------------------------------------------------- /src/setup_cmd.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Stdio 3 | open Common 4 | 5 | let mergetool = 6 | let setup update_git_config echo merge_fmt_path = 7 | let merge_fmt_path = Option.value ~default:"merge-fmt" merge_fmt_path in 8 | let commands = 9 | [ sprintf 10 | "git config --local mergetool.mergefmt.cmd '%s mergetool \ 11 | --base=$BASE --current=$LOCAL --other=$REMOTE -o $MERGED'" 12 | merge_fmt_path 13 | ; "git config --local mergetool.mergefmt.trustExitCode true" 14 | ] 15 | in 16 | List.iter commands ~f:(fun line -> 17 | if update_git_config 18 | then system_respect_exit ~echo "%s" line 19 | else Out_channel.printf "%s\n%!" line) 20 | in 21 | let open Cmdliner in 22 | let merge_fmt_path = 23 | let doc = "Path of merge-fmt." in 24 | Arg.(value & opt (some string) None & info [ "merge-fmt-path" ] ~doc) 25 | in 26 | let update_git_config = 27 | let doc = 28 | "Update the git config of the current repository. Just output commands \ 29 | otherwise." 30 | in 31 | Arg.(value & flag & info [ "update" ] ~doc) 32 | in 33 | let doc = "Register the [merge-fmt] mergetool in git" in 34 | let term = 35 | Term.(const setup $ update_git_config $ Flags.echo $ merge_fmt_path) 36 | in 37 | Cmd.v (Cmd.info ~doc "setup-mergetool") term 38 | 39 | let merge = 40 | let setup update_git_config echo merge_fmt_path = 41 | let merge_fmt_path = Option.value ~default:"merge-fmt" merge_fmt_path in 42 | let commands = 43 | [ sprintf "git config --local merge.mergefmt.name 'merge-fmt driver'" 44 | ; sprintf 45 | "git config --local merge.mergefmt.driver '%s mergetool --base=%%O \ 46 | --current=%%A --other=%%B -o %%A --name=%%P'" 47 | merge_fmt_path 48 | ; sprintf "git config --local merge.tool 'mergefmt'" 49 | ; sprintf "git config --local merge.default 'mergefmt'" 50 | ] 51 | in 52 | List.iter commands ~f:(fun line -> 53 | if update_git_config 54 | then system_respect_exit ~echo "%s" line 55 | else Out_channel.printf "%s\n%!" line) 56 | in 57 | let open Cmdliner in 58 | let merge_fmt_path = 59 | let doc = "Path of merge-fmt." in 60 | Arg.(value & opt (some string) None & info [ "merge-fmt-path" ] ~doc) 61 | in 62 | let update_git_config = 63 | let doc = 64 | "Update the git config of the current repository. Just output commands \ 65 | otherwise." 66 | in 67 | Arg.(value & flag & info [ "update" ] ~doc) 68 | in 69 | let doc = 70 | "Register the [merge-fmt] mergetool as the default merge driver in git" 71 | in 72 | let term = 73 | Term.(const setup $ update_git_config $ Flags.echo $ merge_fmt_path) 74 | in 75 | Cmd.v (Cmd.info ~doc "setup-merge") term 76 | -------------------------------------------------------------------------------- /src/setup_cmd.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Cmdliner 3 | 4 | val mergetool : unit Cmd.t 5 | val merge : unit Cmd.t 6 | -------------------------------------------------------------------------------- /test/common.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | 4 | let ( ^/ ) = Stdlib.Filename.concat 5 | let echo = ref false 6 | 7 | let system fmt = 8 | Printf.ksprintf 9 | (fun s -> 10 | if !echo then eprintf "+ %s\n%!" s; 11 | let p = Core_unix.system s in 12 | match p with 13 | | Ok () -> () 14 | | Error (`Signal _) -> printf "Signaled\n" 15 | | Error (`Exit_non_zero n) -> printf "Exit with %d\n" n) 16 | fmt 17 | 18 | let git_add fn = system "git add %s" fn 19 | let git_commit msg = system "git commit -m %S -q" msg 20 | let git_branch br = system "git branch %s" br 21 | let git_checkout name = system "git checkout %s" name 22 | 23 | let write fn content = 24 | Out_channel.write_all fn ~data:content; 25 | git_add fn 26 | 27 | let print_status () = 28 | let ic = Unix.open_process_in "git status -s" in 29 | let stats = 30 | List.filter_map (In_channel.input_lines ic) ~f:(fun line -> 31 | match 32 | String.split (String.strip line) ~on:' ' 33 | |> List.filter ~f:(function "" -> false | _ -> true) 34 | with 35 | | [ m; filename ] -> Some (m, filename) 36 | | _ -> assert false) 37 | in 38 | match stats with 39 | | [] -> printf "no changes" 40 | | l -> 41 | List.iter l ~f:(fun (m, f) -> 42 | printf "%s File %s\n" m f; 43 | print_endline (In_channel.read_all f)) 44 | 45 | let print_file file = 46 | printf "File %s\n" file; 47 | print_endline (In_channel.read_all file) 48 | 49 | let filter_hint expected = 50 | String.split ~on:'\n' expected 51 | |> List.filter ~f:(fun s -> 52 | not (String.is_prefix ~prefix:"hint:" (String.strip s))) 53 | |> String.concat ~sep:"\n" |> print_string 54 | 55 | let git_init () = 56 | system "git init . -q"; 57 | write ".ocamlformat" "profile=janestreet"; 58 | git_commit "initial" 59 | 60 | let merge_fmt = 61 | let current_dir = Unix.getcwd () in 62 | let tool = "../src/merge_fmt.exe" in 63 | Stdlib.Filename.concat current_dir tool 64 | 65 | let resolve () = system "%s" merge_fmt 66 | 67 | let with_temp_dir f = 68 | let in_dir = Sys.getenv "TMPDIR" in 69 | let keep_tmp_dir = Option.is_some (Sys.getenv "KEEP_EXPECT_TEST_DIR") in 70 | let dir = Filename_unix.temp_dir ?in_dir "expect-" "-test" in 71 | (* Note that this blocks *) 72 | assert (not (Stdlib.Filename.is_relative dir)); 73 | let res = match f dir with x -> Ok x | exception e -> Error e in 74 | if keep_tmp_dir 75 | then eprintf "OUTPUT LEFT IN %s\n" dir 76 | else system "rm -rf %s" dir; 77 | Result.ok_exn res 78 | 79 | let within_temp_dir ?(links = []) f = 80 | let cwd = Unix.getcwd () in 81 | let (_ : int) = Unix.umask 0o077 in 82 | with_temp_dir (fun temp_dir -> 83 | (* disable all external git configuration configuration *) 84 | Core_unix.putenv ~key:"GIT_CONFIG_NOSYSTEM" ~data:"1"; 85 | Core_unix.putenv ~key:"GIT_CONFIG_NOGLOBAL" ~data:"1"; 86 | 87 | Core_unix.unsetenv "GIT_CONFIG"; 88 | Core_unix.unsetenv "HOME"; 89 | Core_unix.unsetenv "XDG_CONFIG_HOME"; 90 | 91 | Core_unix.putenv ~key:"GIT_COMMITTER_NAME" ~data:"John Doe"; 92 | Core_unix.putenv ~key:"GIT_COMMITTER_DATE" 93 | ~data:"2020-12-03 19:00:00 +0000"; 94 | Core_unix.putenv ~key:"GIT_COMMITTER_EMAIL" ~data:"johndoe@doe.com"; 95 | Core_unix.putenv ~key:"GIT_AUTHOR_NAME" ~data:"John Doe"; 96 | Core_unix.putenv ~key:"GIT_AUTHOR_EMAIL" ~data:"johndoe@doe.com"; 97 | Core_unix.putenv ~key:"GIT_AUTHOR_DATE" ~data:"2020-12-03 19:00:00 +0000"; 98 | Core_unix.putenv ~key:"GIT_EDITOR" ~data:"true"; 99 | let path_var = "PATH" in 100 | let old_path = Sys.getenv_exn path_var in 101 | let bin = temp_dir ^/ "bin" in 102 | Core_unix.putenv ~key:path_var 103 | ~data:(String.concat ~sep:":" [ bin; old_path ]); 104 | let () = system "mkdir %s" bin in 105 | let () = 106 | List.iter links ~f:(fun (file, action, link_as) -> 107 | let link_as = 108 | match action with 109 | | `In_path_as -> "bin" ^/ link_as 110 | | `In_temp_as -> link_as 111 | in 112 | (* We use hard links to ensure that files remain available and unchanged even if 113 | jenga starts to rebuild while the test is running. *) 114 | system "/bin/ln -T %s %s" file (temp_dir ^/ link_as)) 115 | in 116 | let () = Unix.chdir temp_dir in 117 | let res = match f () with x -> Ok x | exception e -> Error e in 118 | Core_unix.putenv ~key:path_var ~data:old_path; 119 | Unix.chdir cwd; 120 | Result.ok_exn res) 121 | 122 | let%expect_test _ = 123 | within_temp_dir (fun () -> 124 | git_init (); 125 | system "git config -l"; 126 | [%expect 127 | {| 128 | core.repositoryformatversion=0 129 | core.filemode=true 130 | core.bare=false 131 | core.logallrefupdates=true 132 | |}]; 133 | system "git show --format=raw HEAD"; 134 | [%expect 135 | {| 136 | commit 4de0f8c2fa140c9b4cf667864af6fb76afae0206 137 | tree 90f548f11622b8462d718dfa8a6b5749c67145e7 138 | author John Doe 1607022000 +0000 139 | committer John Doe 1607022000 +0000 140 | 141 | initial 142 | 143 | diff --git a/.ocamlformat b/.ocamlformat 144 | new file mode 100644 145 | index 0000000..fa4af5a 146 | --- /dev/null 147 | +++ b/.ocamlformat 148 | @@ -0,0 +1 @@ 149 | +profile=janestreet 150 | \ No newline at end of file |}]; 151 | write "a.ml" 152 | {| 153 | type t = { a : int; 154 | b : string; 155 | c : float; 156 | } 157 | |}; 158 | git_commit "first commit"; 159 | system "git show --format=raw HEAD"; 160 | [%expect 161 | {| 162 | commit 2a7a47ddd1c03d9e88ab56eac01765014c53b2de 163 | tree c274a479e323ccf6b8b06ed2984b8147f56ca87c 164 | parent 4de0f8c2fa140c9b4cf667864af6fb76afae0206 165 | author John Doe 1607022000 +0000 166 | committer John Doe 1607022000 +0000 167 | 168 | first commit 169 | 170 | diff --git a/a.ml b/a.ml 171 | new file mode 100644 172 | index 0000000..648ba45 173 | --- /dev/null 174 | +++ b/a.ml 175 | @@ -0,0 +1,5 @@ 176 | + 177 | +type t = { a : int; 178 | + b : string; 179 | + c : float; 180 | + } |}]) 181 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name merge_fmt_test) 3 | (libraries base stdio unix core_unix core_unix.filename_unix) 4 | (inline_tests) 5 | (preprocessor_deps ../src/merge_fmt.exe) 6 | (preprocess 7 | (pps ppx_expect))) 8 | 9 | ;; [rebase_a.ml] and [rebase_b.ml] should be the same expect that 10 | ;; [rebase_b.ml] does rebase in an intermediate revision. 11 | 12 | (rule 13 | (targets rebase.diff.gen) 14 | (action 15 | (with-stdout-to 16 | %{targets} 17 | (bash "diff %{dep:rebase_a.ml} %{dep:rebase_b.ml} || true")))) 18 | 19 | (rule 20 | (alias runtest) 21 | (action 22 | (diff rebase.diff rebase.diff.gen))) 23 | -------------------------------------------------------------------------------- /test/merge.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | (* Tests mergetool *) 6 | 7 | let%expect_test "default merge tool" = 8 | within_temp_dir (fun () -> 9 | git_init (); 10 | 11 | (* system "%s setup-merge --merge-fmt-path %s --update" merge_fmt merge_fmt; *) 12 | [%expect {||}]; 13 | write "a.ml" 14 | {| 15 | type t = { a : int; 16 | b : string; 17 | c : float; 18 | } 19 | |}; 20 | git_commit "first commit"; 21 | git_branch "branch1"; 22 | 23 | (* Add new field, move file *) 24 | write "a.ml" 25 | {| 26 | type t = { a : int; 27 | b : string; 28 | c : float; 29 | d : unit option } 30 | |}; 31 | system "git mv a.ml b.ml"; 32 | git_commit "second commit"; 33 | 34 | (* Add new type *) 35 | write "b.ml" 36 | {| 37 | type t = { a : int; 38 | b : string; 39 | c : float; 40 | d : unit option } 41 | 42 | type u = A | B of int 43 | |}; 44 | git_commit "third commit"; 45 | git_branch "branch2"; 46 | 47 | (* Go back to branch1, turn [a] to [int option], reformat *) 48 | git_checkout "branch1"; 49 | write "a.ml" 50 | {| 51 | type t = 52 | { a : int option 53 | ; b : string 54 | ; c : float 55 | } 56 | |}; 57 | git_commit "second commit (fork)"; 58 | [%expect {| Switched to branch 'branch1' |}]; 59 | 60 | (* add new type before *) 61 | write "a.ml" 62 | {| 63 | type b = int 64 | 65 | 66 | type t = 67 | { a : int option 68 | ; b : string 69 | ; c : float 70 | } 71 | |}; 72 | git_commit "third commit (fork)"; 73 | git_branch "old_branch1"; 74 | system "git rebase branch2 -q"; 75 | filter_hint [%expect.output]; 76 | [%expect 77 | {| 78 | Auto-merging b.ml 79 | CONFLICT (content): Merge conflict in b.ml 80 | error: could not apply 6499b96... second commit (fork) 81 | Could not apply 6499b96... second commit (fork) 82 | Exit with 1 83 | |}]; 84 | print_file "b.ml"; 85 | [%expect 86 | {| 87 | File b.ml 88 | 89 | <<<<<<< HEAD:b.ml 90 | type t = { a : int; 91 | b : string; 92 | c : float; 93 | d : unit option } 94 | 95 | type u = A | B of int 96 | ======= 97 | type t = 98 | { a : int option 99 | ; b : string 100 | ; c : float 101 | } 102 | >>>>>>> 6499b96 (second commit (fork)):a.ml |}]) 103 | 104 | let%expect_test "custom merge tool" = 105 | within_temp_dir (fun () -> 106 | git_init (); 107 | system "%s setup-merge --merge-fmt-path %s --update" merge_fmt merge_fmt; 108 | [%expect {||}]; 109 | write "a.ml" 110 | {| 111 | type t = { a : int; 112 | b : string; 113 | c : float; 114 | } 115 | |}; 116 | git_commit "first commit"; 117 | git_branch "branch1"; 118 | 119 | (* Add new field, move file *) 120 | write "a.ml" 121 | {| 122 | type t = { a : int; 123 | b : string; 124 | c : float; 125 | d : unit option } 126 | |}; 127 | system "git mv a.ml b.ml"; 128 | git_commit "second commit"; 129 | 130 | (* Add new type *) 131 | write "b.ml" 132 | {| 133 | type t = { a : int; 134 | b : string; 135 | c : float; 136 | d : unit option } 137 | 138 | type u = A | B of int 139 | |}; 140 | git_commit "third commit"; 141 | git_branch "branch2"; 142 | 143 | (* Go back to branch1, turn [a] to [int option], reformat *) 144 | git_checkout "branch1"; 145 | write "a.ml" 146 | {| 147 | type t = 148 | { a : int option 149 | ; b : string 150 | ; c : float 151 | } 152 | |}; 153 | git_commit "second commit (fork)"; 154 | [%expect {| Switched to branch 'branch1' |}]; 155 | 156 | (* add new type before *) 157 | write "a.ml" 158 | {| 159 | type b = int 160 | 161 | 162 | type t = 163 | { a : int option 164 | ; b : string 165 | ; c : float 166 | } 167 | |}; 168 | git_commit "third commit (fork)"; 169 | git_branch "old_branch1"; 170 | system "git rebase branch2 -q"; 171 | [%expect {| |}]; 172 | print_file "b.ml"; 173 | [%expect 174 | {| 175 | File b.ml 176 | type b = int 177 | 178 | type t = 179 | { a : int option 180 | ; b : string 181 | ; c : float 182 | ; d : unit option 183 | } 184 | 185 | type u = 186 | | A 187 | | B of int |}]) 188 | -------------------------------------------------------------------------------- /test/merge_dune.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | let%expect_test "default merge tool" = 6 | within_temp_dir (fun () -> 7 | git_init (); 8 | 9 | write "dune" {| 10 | (executable 11 | (name aaaa) 12 | (libraries unix)) 13 | |}; 14 | git_commit "first commit"; 15 | git_branch "branch1"; 16 | 17 | (* add public name *) 18 | if false 19 | then ( 20 | write "dune" 21 | {| 22 | (executable 23 | (name aaaa) 24 | (libraries unix) 25 | (public_name pppp)) 26 | |}; 27 | git_commit "second commit"); 28 | 29 | (* add library *) 30 | write "dune" 31 | {| 32 | (executable 33 | (name aaaa) 34 | (libraries unix)) 35 | 36 | (library 37 | (name liblib)) 38 | |}; 39 | git_commit "third commit"; 40 | git_branch "branch2"; 41 | 42 | git_checkout "branch1"; 43 | [%expect {| Switched to branch 'branch1' |}]; 44 | (* change name to b, reformat *) 45 | write "dune" {| 46 | (executable 47 | (name bbbb) 48 | (libraries unix)) 49 | |}; 50 | git_commit "second commit (fork)"; 51 | 52 | write "dune" 53 | {| 54 | (alias 55 | (name runtest) 56 | (action 57 | (diff rebase.diff rebase.diff.gen))) 58 | 59 | (executable 60 | (name bbbb) 61 | (libraries unix)) 62 | |}; 63 | 64 | git_commit "third commit (fork)"; 65 | git_branch "old_branch1"; 66 | system "git rebase branch2 -q"; 67 | filter_hint [%expect.output]; 68 | [%expect 69 | {| 70 | Auto-merging dune 71 | CONFLICT (content): Merge conflict in dune 72 | error: could not apply 2e00bc1... second commit (fork) 73 | Could not apply 2e00bc1... second commit (fork) 74 | Exit with 1 75 | |}]; 76 | print_file "dune"; 77 | [%expect 78 | {| 79 | File dune 80 | 81 | (executable 82 | <<<<<<< HEAD 83 | (name aaaa) 84 | (libraries unix)) 85 | 86 | (library 87 | (name liblib)) 88 | ======= 89 | (name bbbb) 90 | (libraries unix)) 91 | >>>>>>> 2e00bc1 (second commit (fork)) |}]) 92 | 93 | let%expect_test "custom merge tool" = 94 | within_temp_dir (fun () -> 95 | git_init (); 96 | system "%s setup-merge --merge-fmt-path %s --update" merge_fmt merge_fmt; 97 | [%expect {||}]; 98 | 99 | write "dune" {| 100 | (executable 101 | (name aaaa) 102 | (libraries unix)) 103 | |}; 104 | git_commit "first commit"; 105 | git_branch "branch1"; 106 | 107 | (* add public name *) 108 | if false 109 | then ( 110 | write "dune" 111 | {| 112 | (executable 113 | (name aaaa) 114 | (libraries unix) 115 | (public_name pppp)) 116 | |}; 117 | git_commit "second commit"); 118 | 119 | (* add library *) 120 | write "dune" 121 | {| 122 | (executable 123 | (name aaaa) 124 | (libraries unix)) 125 | 126 | (library 127 | (name liblib)) 128 | |}; 129 | git_commit "third commit"; 130 | git_branch "branch2"; 131 | 132 | git_checkout "branch1"; 133 | [%expect {| Switched to branch 'branch1' |}]; 134 | (* change name to b, reformat *) 135 | write "dune" {| 136 | (executable 137 | (name bbbb) 138 | (libraries unix)) 139 | |}; 140 | git_commit "second commit (fork)"; 141 | 142 | write "dune" 143 | {| 144 | (alias 145 | (name runtest) 146 | (action 147 | (diff rebase.diff rebase.diff.gen))) 148 | 149 | (executable 150 | (name bbbb) 151 | (libraries unix)) 152 | |}; 153 | git_commit "third commit (fork)"; 154 | git_branch "old_branch1"; 155 | 156 | system "git rebase branch2 -q"; 157 | [%expect {| |}]; 158 | system "%s" merge_fmt; 159 | print_file "dune"; 160 | [%expect 161 | {| 162 | Nothing to resolve 163 | Exit with 1 164 | File dune 165 | (alias 166 | (name runtest) 167 | (action 168 | (diff rebase.diff rebase.diff.gen))) 169 | 170 | (executable 171 | (name bbbb) 172 | (libraries unix)) 173 | 174 | (library 175 | (name liblib)) |}]) 176 | -------------------------------------------------------------------------------- /test/mergetool.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | (* Tests mergetool *) 6 | 7 | let%expect_test _ = 8 | within_temp_dir (fun () -> 9 | git_init (); 10 | system "%s setup-mergetool --merge-fmt-path %s --update" merge_fmt 11 | merge_fmt; 12 | [%expect {||}]; 13 | write "a.ml" 14 | {| 15 | type t = { a : int; 16 | b : string; 17 | c : float; 18 | } 19 | |}; 20 | git_commit "first commit"; 21 | git_branch "branch1"; 22 | 23 | (* Add new field, move file *) 24 | write "a.ml" 25 | {| 26 | type t = { a : int; 27 | b : string; 28 | c : float; 29 | d : unit option } 30 | |}; 31 | system "git mv a.ml b.ml"; 32 | git_commit "second commit"; 33 | 34 | (* Add new type *) 35 | write "b.ml" 36 | {| 37 | type t = { a : int; 38 | b : string; 39 | c : float; 40 | d : unit option } 41 | 42 | type u = A | B of int 43 | |}; 44 | git_commit "third commit"; 45 | git_branch "branch2"; 46 | 47 | (* Go back to branch1, turn [a] to [int option], reformat *) 48 | git_checkout "branch1"; 49 | write "a.ml" 50 | {| 51 | type t = 52 | { a : int option 53 | ; b : string 54 | ; c : float 55 | } 56 | |}; 57 | git_commit "second commit (fork)"; 58 | [%expect {| Switched to branch 'branch1' |}]; 59 | 60 | (* add new type before *) 61 | write "a.ml" 62 | {| 63 | type b = int 64 | 65 | 66 | type t = 67 | { a : int option 68 | ; b : string 69 | ; c : float 70 | } 71 | |}; 72 | git_commit "third commit (fork)"; 73 | git_branch "old_branch1"; 74 | system "git rebase branch2 -q"; 75 | filter_hint [%expect.output]; 76 | [%expect 77 | {| 78 | Auto-merging b.ml 79 | CONFLICT (content): Merge conflict in b.ml 80 | error: could not apply 6499b96... second commit (fork) 81 | Could not apply 6499b96... second commit (fork) 82 | Exit with 1 83 | |}]; 84 | print_status (); 85 | [%expect 86 | {| 87 | UU File b.ml 88 | 89 | <<<<<<< HEAD:b.ml 90 | type t = { a : int; 91 | b : string; 92 | c : float; 93 | d : unit option } 94 | 95 | type u = A | B of int 96 | ======= 97 | type t = 98 | { a : int option 99 | ; b : string 100 | ; c : float 101 | } 102 | >>>>>>> 6499b96 (second commit (fork)):a.ml |}]; 103 | system "git mergetool --tool mergefmt -y"; 104 | system "git clean -f"; 105 | [%expect 106 | {| 107 | Merging: 108 | b.ml 109 | 110 | Normal merge conflict for 'b.ml': 111 | {local}: modified file 112 | {remote}: modified file 113 | Removing b.ml.orig |}]; 114 | print_status (); 115 | [%expect 116 | {| 117 | M File b.ml 118 | type t = 119 | { a : int option 120 | ; b : string 121 | ; c : float 122 | ; d : unit option 123 | } 124 | 125 | type u = 126 | | A 127 | | B of int |}]; 128 | system "git rebase --continue"; 129 | filter_hint [%expect.output]; 130 | [%expect 131 | {| 132 | [detached HEAD 3fd12a7] second commit (fork) 133 | 1 file changed, 9 insertions(+), 6 deletions(-) 134 | Auto-merging b.ml 135 | CONFLICT (content): Merge conflict in b.ml 136 | error: could not apply bfafc01... third commit (fork) 137 | Could not apply bfafc01... third commit (fork) 138 | Exit with 1 139 | |}]; 140 | print_status (); 141 | [%expect 142 | {| 143 | UU File b.ml 144 | <<<<<<< HEAD:b.ml 145 | ======= 146 | 147 | type b = int 148 | 149 | 150 | >>>>>>> bfafc01 (third commit (fork)):a.ml 151 | type t = 152 | { a : int option 153 | ; b : string 154 | ; c : float 155 | ; d : unit option 156 | } 157 | 158 | type u = 159 | | A 160 | | B of int |}]; 161 | system "git mergetool --tool mergefmt -y"; 162 | system "git clean -f"; 163 | [%expect 164 | {| 165 | Merging: 166 | b.ml 167 | 168 | Normal merge conflict for 'b.ml': 169 | {local}: modified file 170 | {remote}: modified file 171 | Removing b.ml.orig |}]; 172 | print_status (); 173 | [%expect 174 | {| 175 | M File b.ml 176 | type b = int 177 | 178 | type t = 179 | { a : int option 180 | ; b : string 181 | ; c : float 182 | ; d : unit option 183 | } 184 | 185 | type u = 186 | | A 187 | | B of int |}]; 188 | system "git rebase --continue"; 189 | [%expect 190 | {| 191 | [detached HEAD 2df8de0] third commit (fork) 192 | 1 file changed, 2 insertions(+) |}]) 193 | -------------------------------------------------------------------------------- /test/partial.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | (* Tests partial resolution *) 6 | 7 | let%expect_test _ = 8 | within_temp_dir (fun () -> 9 | git_init (); 10 | write "a.ml" 11 | {| 12 | type t = { a : int; 13 | b : string; 14 | c : float; 15 | } 16 | |}; 17 | git_commit "first"; 18 | git_branch "branch1"; 19 | write "a.ml" 20 | {| 21 | type t = { a : int; 22 | b : string; 23 | c : float; 24 | d : unit option } 25 | 26 | let y = 0 27 | 28 | (** doc *)xs 29 | let x = 1 30 | |}; 31 | system "git mv a.ml b.ml"; 32 | git_commit "second"; 33 | write "b.ml" 34 | {| 35 | type t = { a : int; 36 | b : string; 37 | c : float; 38 | d : unit option } 39 | 40 | let y = 0 41 | 42 | (** doc *) 43 | let x = 1 44 | |}; 45 | git_commit "third"; 46 | git_branch "branch2"; 47 | git_checkout "branch1"; 48 | write "a.ml" 49 | {| 50 | type t = 51 | { a : int option 52 | ; b : string 53 | ; c : float 54 | } 55 | 56 | let y = 0 57 | 58 | (** doc *) 59 | let x = 5 60 | |}; 61 | git_commit "second prime"; 62 | git_branch "old_branch1"; 63 | [%expect {| Switched to branch 'branch1' |}]; 64 | system "git rebase branch2 -q"; 65 | filter_hint [%expect.output]; 66 | [%expect 67 | {| 68 | Auto-merging b.ml 69 | CONFLICT (content): Merge conflict in b.ml 70 | error: could not apply 10b6b8e... second prime 71 | Could not apply 10b6b8e... second prime 72 | Exit with 1 73 | |}]; 74 | print_status (); 75 | [%expect 76 | {| 77 | UU File b.ml 78 | 79 | <<<<<<< HEAD:b.ml 80 | type t = { a : int; 81 | b : string; 82 | c : float; 83 | d : unit option } 84 | ======= 85 | type t = 86 | { a : int option 87 | ; b : string 88 | ; c : float 89 | } 90 | >>>>>>> 10b6b8e (second prime):a.ml 91 | 92 | let y = 0 93 | 94 | (** doc *) 95 | <<<<<<< HEAD:b.ml 96 | let x = 1 97 | ======= 98 | let x = 5 99 | >>>>>>> 10b6b8e (second prime):a.ml |}]; 100 | resolve (); 101 | print_status (); 102 | [%expect 103 | {| 104 | Resolved 1/2 b.ml 105 | Exit with 1 106 | UU File b.ml 107 | type t = 108 | { a : int option 109 | ; b : string 110 | ; c : float 111 | ; d : unit option 112 | } 113 | 114 | let y = 0 115 | 116 | (** doc *) 117 | <<<<<<< b.ours.ml 118 | let x = 1 119 | ======= 120 | let x = 5 121 | >>>>>>> b.theirs.ml 122 | 123 | ?? File b.common.ml 124 | type t = 125 | { a : int 126 | ; b : string 127 | ; c : float 128 | } 129 | 130 | ?? File b.ours.ml 131 | type t = 132 | { a : int 133 | ; b : string 134 | ; c : float 135 | ; d : unit option 136 | } 137 | 138 | let y = 0 139 | 140 | (** doc *) 141 | let x = 1 142 | 143 | ?? File b.theirs.ml 144 | type t = 145 | { a : int option 146 | ; b : string 147 | ; c : float 148 | } 149 | 150 | let y = 0 151 | 152 | (** doc *) 153 | let x = 5 |}]; 154 | system "git merge-file -p b.ours.ml b.common.ml b.theirs.ml --ours"; 155 | [%expect 156 | {| 157 | type t = 158 | { a : int option 159 | ; b : string 160 | ; c : float 161 | ; d : unit option 162 | } 163 | 164 | let y = 0 165 | 166 | (** doc *) 167 | let x = 1 |}]; 168 | system "git add b.ml"; 169 | [%expect {||}]; 170 | system "git rebase --continue"; 171 | [%expect 172 | {| 173 | [detached HEAD 7627cfc] second prime 174 | 1 file changed, 10 insertions(+), 5 deletions(-) |}]) 175 | -------------------------------------------------------------------------------- /test/rebase.diff: -------------------------------------------------------------------------------- 1 | 36a37,40 2 | > system "git rebase branch2^ -q"; 3 | > print_status (); 4 | > [%expect {| 5 | > no changes |}]; 6 | 41,43c45,48 7 | < CONFLICT (modify/delete): a.ml deleted in HEAD and modified in 5f62452 (second commit (fork)). Version 5f62452 (second commit (fork)) of a.ml left in tree. 8 | < error: could not apply 5f62452... second commit (fork) 9 | < Could not apply 5f62452... second commit (fork) 10 | --- 11 | > Auto-merging b.ml 12 | > CONFLICT (content): Merge conflict in b.ml 13 | > error: could not apply 6070a8f... second commit (fork) 14 | > Could not apply 6070a8f... second commit (fork) 15 | 49c54 16 | < DU File a.ml 17 | --- 18 | > UU File b.ml 19 | 50a56,59 20 | > <<<<<<< HEAD 21 | > type t = { a : int; b : string; 22 | > c : float; d : unit option } 23 | > ======= 24 | 55c64,65 25 | < } |}]; 26 | --- 27 | > } 28 | > >>>>>>> 6070a8f (second commit (fork)) |}]; 29 | 57,60c67,68 30 | < [%expect 31 | < {| 32 | < Ignore a.ml (not a 3-way merge) 33 | < Exit with 1 |}]; 34 | --- 35 | > [%expect {| 36 | > Resolved 1/1 b.ml |}]; 37 | 64,65c72 38 | < DU File a.ml 39 | < 40 | --- 41 | > M File b.ml 42 | 67,69c74,77 43 | < { a : int option; 44 | < b : string; 45 | < c : float; 46 | --- 47 | > { a : int option 48 | > ; b : string 49 | > ; c : float 50 | > ; d : unit option 51 | 74,77c82,83 52 | < a.ml: needs merge 53 | < You must edit all merge conflicts and then 54 | < mark them as resolved using git add 55 | < Exit with 1 |}]) 56 | --- 57 | > [detached HEAD b70d467] second commit (fork) 58 | > 1 file changed, 6 insertions(+), 3 deletions(-) |}]) 59 | -------------------------------------------------------------------------------- /test/rebase_a.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | let%expect_test _ = 6 | within_temp_dir (fun () -> 7 | git_init (); 8 | write "a.ml" 9 | {| 10 | type t = { a : int; 11 | b : string; 12 | c : float } 13 | |}; 14 | git_commit "first commit"; 15 | git_branch "branch1"; 16 | system "git mv a.ml b.ml"; 17 | git_commit "move a to b"; 18 | write "b.ml" 19 | {| 20 | type t = { a : int; b : string; 21 | c : float; d : unit option } 22 | |}; 23 | git_commit "second commit"; 24 | git_branch "branch2"; 25 | git_checkout "branch1"; 26 | write "a.ml" 27 | {| 28 | type t = 29 | { a : int option; 30 | b : string; 31 | c : float; 32 | } 33 | |}; 34 | git_commit "second commit (fork)"; 35 | git_branch "old_branch1"; 36 | [%expect {| Switched to branch 'branch1' |}]; 37 | system "git rebase branch2 -q"; 38 | filter_hint [%expect.output]; 39 | [%expect 40 | {| 41 | CONFLICT (modify/delete): a.ml deleted in HEAD and modified in 5f62452 (second commit (fork)). Version 5f62452 (second commit (fork)) of a.ml left in tree. 42 | error: could not apply 5f62452... second commit (fork) 43 | Could not apply 5f62452... second commit (fork) 44 | Exit with 1 45 | |}]; 46 | print_status (); 47 | [%expect 48 | {| 49 | DU File a.ml 50 | 51 | type t = 52 | { a : int option; 53 | b : string; 54 | c : float; 55 | } |}]; 56 | resolve (); 57 | [%expect 58 | {| 59 | Ignore a.ml (not a 3-way merge) 60 | Exit with 1 |}]; 61 | print_status (); 62 | [%expect 63 | {| 64 | DU File a.ml 65 | 66 | type t = 67 | { a : int option; 68 | b : string; 69 | c : float; 70 | } |}]; 71 | system "git rebase --continue"; 72 | [%expect 73 | {| 74 | a.ml: needs merge 75 | You must edit all merge conflicts and then 76 | mark them as resolved using git add 77 | Exit with 1 |}]) 78 | -------------------------------------------------------------------------------- /test/rebase_b.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | let%expect_test _ = 6 | within_temp_dir (fun () -> 7 | git_init (); 8 | write "a.ml" 9 | {| 10 | type t = { a : int; 11 | b : string; 12 | c : float } 13 | |}; 14 | git_commit "first commit"; 15 | git_branch "branch1"; 16 | system "git mv a.ml b.ml"; 17 | git_commit "move a to b"; 18 | write "b.ml" 19 | {| 20 | type t = { a : int; b : string; 21 | c : float; d : unit option } 22 | |}; 23 | git_commit "second commit"; 24 | git_branch "branch2"; 25 | git_checkout "branch1"; 26 | write "a.ml" 27 | {| 28 | type t = 29 | { a : int option; 30 | b : string; 31 | c : float; 32 | } 33 | |}; 34 | git_commit "second commit (fork)"; 35 | git_branch "old_branch1"; 36 | [%expect {| Switched to branch 'branch1' |}]; 37 | system "git rebase branch2^ -q"; 38 | print_status (); 39 | [%expect {| 40 | no changes |}]; 41 | system "git rebase branch2 -q"; 42 | filter_hint [%expect.output]; 43 | [%expect 44 | {| 45 | Auto-merging b.ml 46 | CONFLICT (content): Merge conflict in b.ml 47 | error: could not apply 6070a8f... second commit (fork) 48 | Could not apply 6070a8f... second commit (fork) 49 | Exit with 1 50 | |}]; 51 | print_status (); 52 | [%expect 53 | {| 54 | UU File b.ml 55 | 56 | <<<<<<< HEAD 57 | type t = { a : int; b : string; 58 | c : float; d : unit option } 59 | ======= 60 | type t = 61 | { a : int option; 62 | b : string; 63 | c : float; 64 | } 65 | >>>>>>> 6070a8f (second commit (fork)) |}]; 66 | resolve (); 67 | [%expect {| 68 | Resolved 1/1 b.ml |}]; 69 | print_status (); 70 | [%expect 71 | {| 72 | M File b.ml 73 | type t = 74 | { a : int option 75 | ; b : string 76 | ; c : float 77 | ; d : unit option 78 | } |}]; 79 | system "git rebase --continue"; 80 | [%expect 81 | {| 82 | [detached HEAD b70d467] second commit (fork) 83 | 1 file changed, 6 insertions(+), 3 deletions(-) |}]) 84 | -------------------------------------------------------------------------------- /test/resolve1.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | let%expect_test _ = 6 | within_temp_dir (fun () -> 7 | git_init (); 8 | write "a.ml" 9 | {| 10 | type t = { a : int; 11 | b : string; 12 | c : float; 13 | } 14 | |}; 15 | git_commit "first commit"; 16 | git_branch "branch1"; 17 | 18 | (* Add new field, move file *) 19 | write "a.ml" 20 | {| 21 | type t = { a : int; 22 | b : string; 23 | c : float; 24 | d : unit option } 25 | |}; 26 | system "git mv a.ml b.ml"; 27 | git_commit "second commit"; 28 | 29 | (* Add new type *) 30 | write "b.ml" 31 | {| 32 | type t = { a : int; 33 | b : string; 34 | c : float; 35 | d : unit option } 36 | 37 | type u = A | B of int 38 | |}; 39 | git_commit "third commit"; 40 | git_branch "branch2"; 41 | 42 | (* Go back to branch1, turn [a] to [int option], reformat *) 43 | git_checkout "branch1"; 44 | write "a.ml" 45 | {| 46 | type t = 47 | { a : int option 48 | ; b : string 49 | ; c : float 50 | } 51 | |}; 52 | git_commit "second commit (fork)"; 53 | [%expect {| Switched to branch 'branch1' |}]; 54 | 55 | (* add new type before *) 56 | write "a.ml" 57 | {| 58 | type b = int 59 | 60 | 61 | type t = 62 | { a : int option 63 | ; b : string 64 | ; c : float 65 | } 66 | |}; 67 | git_commit "third commit (fork)"; 68 | git_branch "old_branch1"; 69 | system "git rebase branch2 -q"; 70 | filter_hint [%expect.output]; 71 | [%expect 72 | {| 73 | Auto-merging b.ml 74 | CONFLICT (content): Merge conflict in b.ml 75 | error: could not apply 6499b96... second commit (fork) 76 | Could not apply 6499b96... second commit (fork) 77 | Exit with 1 78 | |}]; 79 | print_status (); 80 | [%expect 81 | {| 82 | UU File b.ml 83 | 84 | <<<<<<< HEAD:b.ml 85 | type t = { a : int; 86 | b : string; 87 | c : float; 88 | d : unit option } 89 | 90 | type u = A | B of int 91 | ======= 92 | type t = 93 | { a : int option 94 | ; b : string 95 | ; c : float 96 | } 97 | >>>>>>> 6499b96 (second commit (fork)):a.ml |}]; 98 | resolve (); 99 | print_status (); 100 | [%expect 101 | {| 102 | Resolved 1/1 b.ml 103 | M File b.ml 104 | type t = 105 | { a : int option 106 | ; b : string 107 | ; c : float 108 | ; d : unit option 109 | } 110 | 111 | type u = 112 | | A 113 | | B of int |}]; 114 | system "git rebase --continue"; 115 | filter_hint [%expect.output]; 116 | [%expect 117 | {| 118 | [detached HEAD 3fd12a7] second commit (fork) 119 | 1 file changed, 9 insertions(+), 6 deletions(-) 120 | Auto-merging b.ml 121 | CONFLICT (content): Merge conflict in b.ml 122 | error: could not apply bfafc01... third commit (fork) 123 | Could not apply bfafc01... third commit (fork) 124 | Exit with 1 125 | |}]; 126 | print_status (); 127 | [%expect 128 | {| 129 | UU File b.ml 130 | <<<<<<< HEAD:b.ml 131 | ======= 132 | 133 | type b = int 134 | 135 | 136 | >>>>>>> bfafc01 (third commit (fork)):a.ml 137 | type t = 138 | { a : int option 139 | ; b : string 140 | ; c : float 141 | ; d : unit option 142 | } 143 | 144 | type u = 145 | | A 146 | | B of int |}]; 147 | resolve (); 148 | [%expect {| 149 | Resolved 1/1 b.ml |}]; 150 | print_status (); 151 | [%expect 152 | {| 153 | M File b.ml 154 | type b = int 155 | 156 | type t = 157 | { a : int option 158 | ; b : string 159 | ; c : float 160 | ; d : unit option 161 | } 162 | 163 | type u = 164 | | A 165 | | B of int |}]; 166 | system "git rebase --continue"; 167 | [%expect 168 | {| 169 | [detached HEAD 2df8de0] third commit (fork) 170 | 1 file changed, 2 insertions(+) |}]) 171 | -------------------------------------------------------------------------------- /test/resolve2.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Stdio 3 | open! Common 4 | 5 | let%expect_test _ = 6 | within_temp_dir (fun () -> 7 | git_init (); 8 | write "a.ml" 9 | {| 10 | type t = { a : int; 11 | b : string; 12 | c : float; 13 | } 14 | |}; 15 | git_commit "first commit"; 16 | git_branch "branch1"; 17 | write "a.ml" 18 | {| 19 | type t = { a : int; 20 | b : string; 21 | c : float; 22 | d : unit option 23 | } 24 | |}; 25 | system "git mv a.ml b.ml"; 26 | git_commit "second commit"; 27 | git_branch "branch2"; 28 | [%expect {| |}]; 29 | git_checkout "branch1"; 30 | write "a.ml" 31 | {| 32 | type t = 33 | { a : int option; 34 | b : string; 35 | c : float; 36 | } 37 | |}; 38 | git_commit "second commit (fork)"; 39 | git_branch "old_branch1"; 40 | [%expect {| Switched to branch 'branch1' |}]; 41 | system "git rebase branch2 -q"; 42 | filter_hint [%expect.output]; 43 | [%expect 44 | {| 45 | Auto-merging b.ml 46 | CONFLICT (content): Merge conflict in b.ml 47 | error: could not apply ead71ee... second commit (fork) 48 | Could not apply ead71ee... second commit (fork) 49 | Exit with 1 50 | |}]; 51 | print_status (); 52 | [%expect 53 | {| 54 | UU File b.ml 55 | 56 | <<<<<<< HEAD:b.ml 57 | type t = { a : int; 58 | b : string; 59 | c : float; 60 | d : unit option 61 | } 62 | ======= 63 | type t = 64 | { a : int option; 65 | b : string; 66 | c : float; 67 | } 68 | >>>>>>> ead71ee (second commit (fork)):a.ml |}]; 69 | resolve (); 70 | [%expect {| 71 | Resolved 1/1 b.ml |}]; 72 | print_status (); 73 | [%expect 74 | {| 75 | M File b.ml 76 | type t = 77 | { a : int option 78 | ; b : string 79 | ; c : float 80 | ; d : unit option 81 | } |}]; 82 | system "git rebase --continue"; 83 | [%expect 84 | {| 85 | [detached HEAD f55718f] second commit (fork) 86 | 1 file changed, 6 insertions(+), 6 deletions(-) |}]) 87 | --------------------------------------------------------------------------------