main
rb 122 lines 4.9 KB
Raw
1 # frozen_string_literal: true
2
3 require "rails_helper"
4 require "tmpdir"
5 require "fileutils"
6
7 # Covers the git plumbing that backs pull request merging: comparing two
8 # branches and creating a merge commit without a working tree (the app's
9 # repos are bare).
10 RSpec.describe GitRepositoryService do
11 around do |example|
12 Dir.mktmpdir("git-merge-spec") do |tmp|
13 @repo_dir = File.join(tmp, "demo.git")
14 build_repo(@repo_dir)
15 example.run
16 end
17 end
18
19 describe ".commits_between" do
20 it "lists commits reachable from head but not base" do
21 commits = described_class.commits_between(@repo_dir, "main", "feature")
22 expect(commits.map { |c| c[:subject] }).to eq([ "feature commit" ])
23 end
24
25 it "returns nothing once the branches are equal" do
26 expect(described_class.commits_between(@repo_dir, "main", "main")).to eq([])
27 end
28 end
29
30 describe ".diff" do
31 it "includes the file added on the feature branch" do
32 diff = described_class.diff(@repo_dir, "main", "feature")
33 expect(diff).to include("feature.txt")
34 end
35 end
36
37 describe ".mergeable?" do
38 it "is true for a clean merge" do
39 expect(described_class.mergeable?(@repo_dir, "main", "feature")).to be(true)
40 end
41
42 it "is false when both branches touch the same lines" do
43 expect(described_class.mergeable?(@repo_dir, "conflict-a", "conflict-b")).to be(false)
44 end
45 end
46
47 describe ".merge!" do
48 it "creates a merge commit and advances base" do
49 before_sha = described_class.commit_sha(@repo_dir, "main")
50 sha = described_class.merge!(
51 @repo_dir, base: "main", head: "feature", message: "Merge feature",
52 author_name: "Test Merger", author_email: "merger@example.com"
53 )
54
55 expect(sha).to be_present
56 expect(described_class.commit_sha(@repo_dir, "main")).to eq(sha)
57 expect(described_class.commit_sha(@repo_dir, "main")).not_to eq(before_sha)
58
59 commit = described_class.commit(@repo_dir, sha)
60 expect(commit[:author_name]).to eq("Test Merger")
61 expect(commit[:author_email]).to eq("merger@example.com")
62
63 # Feature's file is now reachable from main.
64 expect(described_class.file_content(@repo_dir, "main", "feature.txt")).to eq("hi\n")
65 end
66
67 it "returns nil and leaves base untouched on conflict" do
68 before_sha = described_class.commit_sha(@repo_dir, "conflict-a")
69 sha = described_class.merge!(
70 @repo_dir, base: "conflict-a", head: "conflict-b", message: "Merge",
71 author_name: "Test", author_email: "test@example.com"
72 )
73
74 expect(sha).to be_nil
75 expect(described_class.commit_sha(@repo_dir, "conflict-a")).to eq(before_sha)
76 end
77
78 it "returns nil for a missing branch" do
79 sha = described_class.merge!(
80 @repo_dir, base: "main", head: "does-not-exist", message: "Merge",
81 author_name: "Test", author_email: "test@example.com"
82 )
83 expect(sha).to be_nil
84 end
85 end
86
87 def build_repo(bare_path)
88 FileUtils.mkdir_p(bare_path)
89 system("git", "init", "--bare", bare_path, exception: true)
90 Dir.mktmpdir do |work|
91 system("git", "-C", work, "init", "-b", "main", exception: true)
92 system("git", "-C", work, "config", "user.email", "t@example.com", exception: true)
93 system("git", "-C", work, "config", "user.name", "Test", exception: true)
94 File.write(File.join(work, "README.md"), "# Demo\n")
95 system("git", "-C", work, "add", ".", exception: true)
96 system("git", "-C", work, "commit", "-m", "seed", exception: true)
97 system("git", "-C", work, "remote", "add", "origin", bare_path, exception: true)
98 system("git", "-C", work, "push", "origin", "main", exception: true)
99
100 system("git", "-C", work, "checkout", "-b", "feature", exception: true)
101 File.write(File.join(work, "feature.txt"), "hi\n")
102 system("git", "-C", work, "add", ".", exception: true)
103 system("git", "-C", work, "commit", "-m", "feature commit", exception: true)
104 system("git", "-C", work, "push", "origin", "feature", exception: true)
105
106 # Two branches that both edit line 1 of the same file — a genuine conflict.
107 system("git", "-C", work, "checkout", "main", exception: true)
108 system("git", "-C", work, "checkout", "-b", "conflict-a", exception: true)
109 File.write(File.join(work, "shared.txt"), "a\n")
110 system("git", "-C", work, "add", ".", exception: true)
111 system("git", "-C", work, "commit", "-m", "conflict a", exception: true)
112 system("git", "-C", work, "push", "origin", "conflict-a", exception: true)
113
114 system("git", "-C", work, "checkout", "main", exception: true)
115 system("git", "-C", work, "checkout", "-b", "conflict-b", exception: true)
116 File.write(File.join(work, "shared.txt"), "b\n")
117 system("git", "-C", work, "add", ".", exception: true)
118 system("git", "-C", work, "commit", "-m", "conflict b", exception: true)
119 system("git", "-C", work, "push", "origin", "conflict-b", exception: true)
120 end
121 end
122 end