summaryrefslogtreecommitdiffstats
path: root/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
blob: c9a474fa9e386e6284308ed18dfa266a85ff97f6 (plain)
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
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.acceptance;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.reviewdb.client.Project;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.TagCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.util.FS;

public class GitUtil {
  private static final AtomicInteger testRepoCount = new AtomicInteger();
  private static final int TEST_REPO_WINDOW_DAYS = 2;

  public static void initSsh(TestAccount a) {
    final Properties config = new Properties();
    config.put("StrictHostKeyChecking", "no");
    JSch.setConfig(config);

    // register a JschConfigSessionFactory that adds the private key as identity
    // to the JSch instance of JGit so that SSH communication via JGit can
    // succeed
    SshSessionFactory.setInstance(
        new JschConfigSessionFactory() {
          @Override
          protected void configure(Host hc, Session session) {
            try {
              final JSch jsch = getJSch(hc, FS.DETECTED);
              jsch.addIdentity("KeyPair", a.privateKey(), a.sshKey.getPublicKeyBlob(), null);
            } catch (JSchException e) {
              throw new RuntimeException(e);
            }
          }
        });
  }

  /**
   * Create a new {@link TestRepository} with a distinct commit clock.
   *
   * <p>It is very easy for tests to create commits with identical subjects and trees; if such
   * commits also have identical authors/committers, then the computed Change-Id is identical as
   * well. Tests may generally assume that Change-Ids are unique, so to ensure this, we provision
   * TestRepository instances with non-overlapping commit clock times.
   *
   * <p>Space test repos 1 day apart, which allows for about 86k ticks per repo before overlapping,
   * and about 8k instances per process before hitting JGit's year 2038 limit.
   *
   * @param repo repository to wrap.
   * @return wrapped test repository with distinct commit time space.
   */
  public static <R extends Repository> TestRepository<R> newTestRepository(R repo)
      throws IOException {
    TestRepository<R> tr = new TestRepository<>(repo);
    tr.tick(
        Ints.checkedCast(
            TimeUnit.SECONDS.convert(
                testRepoCount.getAndIncrement() * TEST_REPO_WINDOW_DAYS, TimeUnit.DAYS)));
    return tr;
  }

  public static TestRepository<InMemoryRepository> cloneProject(Project.NameKey project, String uri)
      throws Exception {
    DfsRepositoryDescription desc = new DfsRepositoryDescription("clone of " + project.get());

    FS fs = FS.detect();

    // Avoid leaking user state into our tests.
    fs.setUserHome(null);

    InMemoryRepository dest =
        new InMemoryRepository.Builder()
            .setRepositoryDescription(desc)
            // SshTransport depends on a real FS to read ~/.ssh/config, but
            // InMemoryRepository by default uses a null FS.
            // TODO(dborowitz): Remove when we no longer depend on SSH.
            .setFS(fs)
            .build();
    Config cfg = dest.getConfig();
    cfg.setString("remote", "origin", "url", uri);
    cfg.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*");
    TestRepository<InMemoryRepository> testRepo = newTestRepository(dest);
    FetchResult result = testRepo.git().fetch().setRemote("origin").call();
    String originMaster = "refs/remotes/origin/master";
    if (result.getTrackingRefUpdate(originMaster) != null) {
      testRepo.reset(originMaster);
    }
    return testRepo;
  }

  public static TestRepository<InMemoryRepository> cloneProject(
      Project.NameKey project, SshSession sshSession) throws Exception {
    return cloneProject(project, sshSession.getUrl() + "/" + project.get());
  }

  public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
      throws GitAPIException {
    TagCommand cmd =
        testRepo.git().tag().setName(name).setAnnotated(true).setMessage(name).setTagger(tagger);
    return cmd.call();
  }

  public static Ref updateAnnotatedTag(TestRepository<?> testRepo, String name, PersonIdent tagger)
      throws GitAPIException {
    TagCommand tc = testRepo.git().tag().setName(name);
    return tc.setAnnotated(true).setMessage(name).setTagger(tagger).setForceUpdate(true).call();
  }

  public static void fetch(TestRepository<?> testRepo, String spec) throws GitAPIException {
    FetchCommand fetch = testRepo.git().fetch();
    fetch.setRefSpecs(new RefSpec(spec));
    fetch.call();
  }

  public static PushResult pushHead(TestRepository<?> testRepo, String ref) throws GitAPIException {
    return pushHead(testRepo, ref, false);
  }

  public static PushResult pushHead(TestRepository<?> testRepo, String ref, boolean pushTags)
      throws GitAPIException {
    return pushHead(testRepo, ref, pushTags, false);
  }

  public static PushResult pushHead(
      TestRepository<?> testRepo, String ref, boolean pushTags, boolean force)
      throws GitAPIException {
    return pushOne(testRepo, "HEAD", ref, pushTags, force, null);
  }

  public static PushResult pushHead(
      TestRepository<?> testRepo,
      String ref,
      boolean pushTags,
      boolean force,
      List<String> pushOptions)
      throws GitAPIException {
    return pushOne(testRepo, "HEAD", ref, pushTags, force, pushOptions);
  }

  public static PushResult deleteRef(TestRepository<?> testRepo, String ref)
      throws GitAPIException {
    return pushOne(testRepo, "", ref, false, true, null);
  }

  public static PushResult pushOne(
      TestRepository<?> testRepo,
      String source,
      String target,
      boolean pushTags,
      boolean force,
      List<String> pushOptions)
      throws GitAPIException {
    PushCommand pushCmd = testRepo.git().push();
    pushCmd.setForce(force);
    pushCmd.setPushOptions(pushOptions);
    pushCmd.setRefSpecs(new RefSpec(source + ":" + target));
    if (pushTags) {
      pushCmd.setPushTags();
    }
    Iterable<PushResult> r = pushCmd.call();
    return Iterables.getOnlyElement(r);
  }

  public static void assertPushOk(PushResult result, String ref) {
    RemoteRefUpdate rru = result.getRemoteUpdate(ref);
    assertThat(rru.getStatus()).named(rru.toString()).isEqualTo(RemoteRefUpdate.Status.OK);
  }

  public static void assertPushRejected(PushResult result, String ref, String expectedMessage) {
    RemoteRefUpdate rru = result.getRemoteUpdate(ref);
    assertThat(rru.getStatus())
        .named(rru.toString())
        .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
    assertThat(rru.getMessage()).isEqualTo(expectedMessage);
  }

  public static PushResult pushTag(TestRepository<?> testRepo, String tag) throws GitAPIException {
    return pushTag(testRepo, tag, false);
  }

  public static PushResult pushTag(TestRepository<?> testRepo, String tag, boolean force)
      throws GitAPIException {
    PushCommand pushCmd = testRepo.git().push();
    pushCmd.setForce(force);
    pushCmd.setRefSpecs(new RefSpec("refs/tags/" + tag + ":refs/tags/" + tag));
    Iterable<PushResult> r = pushCmd.call();
    return Iterables.getOnlyElement(r);
  }

  public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id) throws IOException {
    RevCommit c = tr.getRevWalk().parseCommit(id);
    tr.getRevWalk().parseBody(c);
    return Lists.reverse(c.getFooterLines(FooterConstants.CHANGE_ID)).stream().findFirst();
  }
}