4d05289f179004c0c5dafb61c92de7ea9e51f940
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / Cleanup.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git;
18
19 import com.intellij.execution.configurations.GeneralCommandLine;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.util.Pair;
22 import jetbrains.buildServer.ExecResult;
23 import jetbrains.buildServer.SimpleCommandLineProcessRunner;
24 import jetbrains.buildServer.log.Loggers;
25 import jetbrains.buildServer.util.Dates;
26 import jetbrains.buildServer.util.FileUtil;
27 import jetbrains.buildServer.util.StringUtil;
28 import jetbrains.buildServer.vcs.VcsException;
29 import org.eclipse.jgit.internal.storage.file.FileRepository;
30 import org.eclipse.jgit.internal.storage.file.PackFile;
31 import org.eclipse.jgit.lib.Repository;
32 import org.eclipse.jgit.lib.RepositoryBuilder;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import java.io.File;
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.concurrent.Semaphore;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.atomic.AtomicReference;
44 import java.util.concurrent.locks.Lock;
45 import java.util.function.Consumer;
46 import java.util.regex.Pattern;
47
48 public class Cleanup {
49
50   private static final Logger LOG = Loggers.CLEANUP;
51   private static final Pattern PATTERN_LOOSE_OBJECT = Pattern.compile("[0-9a-fA-F]{38}");
52   private static final Semaphore ourSemaphore = new Semaphore(1);
53
54   private final RepositoryManager myRepositoryManager;
55   private final ServerPluginConfig myConfig;
56   private final GcErrors myGcErrors;
57   private final AtomicReference<RunGitError> myNativeGitError = new AtomicReference<>();
58   @NotNull
59   private volatile Consumer<Runnable> myCleanupCallWrapper = Runnable::run;
60
61   public Cleanup(@NotNull final ServerPluginConfig config,
62                  @NotNull final RepositoryManager repositoryManager,
63                  @NotNull final GcErrors gcErrors) {
64     myConfig = config;
65     myRepositoryManager = repositoryManager;
66     myGcErrors = gcErrors;
67   }
68
69   public void run() {
70     if (!ourSemaphore.tryAcquire()) {
71       LOG.info("Skip git cleanup: another git cleanup process is running");
72       return;
73     }
74
75     try {
76       LOG.info("Git cleanup started");
77       myCleanupCallWrapper.accept(() -> {
78         removeUnusedRepositories();
79         cleanupMonitoringData();
80         if (myConfig.isRunNativeGC()) {
81           runNativeGC();
82         } else if (myConfig.isRunJGitGC()) {
83           runJGitGC();
84         }
85       });
86       LOG.info("Git cleanup finished");
87     } finally {
88       ourSemaphore.release();
89     }
90   }
91
92   public void setCleanupCallWrapper(@NotNull Consumer<Runnable> cleanupCallWrapper) {
93     myCleanupCallWrapper = cleanupCallWrapper;
94   }
95
96   private void removeUnusedRepositories() {
97     List<File> unusedDirs = getUnusedDirs();
98     LOG.debug("Remove unused git repository clones started");
99     for (File dir : unusedDirs) {
100       LOG.info("Remove unused git repository dir " + dir.getAbsolutePath());
101       Lock rmLock = myRepositoryManager.getRmLock(dir).writeLock();
102       rmLock.lock();
103       boolean deleted = false;
104       try {
105         deleted = FileUtil.delete(dir);
106       } finally {
107         rmLock.unlock();
108       }
109       if (deleted) {
110         myGcErrors.clearError(dir);
111         LOG.debug("Remove unused git repository dir " + dir.getAbsolutePath() + " finished");
112       } else {
113         LOG.error("Cannot delete unused git repository dir " + dir.getAbsolutePath());
114         myRepositoryManager.invalidate(dir);
115       }
116     }
117     LOG.debug("Remove unused git repository clones finished");
118   }
119
120   @NotNull
121   private List<File> getUnusedDirs() {
122     return myRepositoryManager.getExpiredDirs();
123   }
124
125   private List<File> getAllRepositoryDirs() {
126     List<File> dirs = new ArrayList<>();
127     for (File d: FileUtil.getSubDirectories(myRepositoryManager.getBaseMirrorsDir())) {
128       if (d.getName().endsWith(".git")) { // there can be some other directories like .gc or .old, we should ignore them
129         dirs.add(d);
130       }
131     }
132     return dirs;
133   }
134
135   private void cleanupMonitoringData() {
136     LOG.debug("Start cleaning git monitoring data");
137     for (File repository : getAllRepositoryDirs()) {
138       File monitoring = new File(repository, myConfig.getMonitoringDirName());
139       File[] files = monitoring.listFiles();
140       if (files != null) {
141         for (File monitoringData : files) {
142           if (isExpired(monitoringData)) {
143             LOG.debug("Remove old git monitoring data " + monitoringData.getAbsolutePath());
144             FileUtil.delete(monitoringData);
145           }
146         }
147       }
148     }
149     LOG.debug("Finish cleaning git monitoring data");
150   }
151
152   private boolean isExpired(@NotNull File f) {
153     long age = System.currentTimeMillis() - f.lastModified();
154     long ageHours = age / Dates.ONE_HOUR;
155     return ageHours > myConfig.getMonitoringExpirationTimeoutHours();
156   }
157
158   private void runNativeGC() {
159     final long startNanos = System.nanoTime();
160     final long gcTimeQuotaNanos = TimeUnit.MINUTES.toNanos(myConfig.getNativeGCQuotaMinutes());
161     List<File> allDirs = getAllRepositoryDirs();
162     myGcErrors.retainErrors(allDirs);
163     if (allDirs.isEmpty()) {
164       LOG.debug("No repositories found");
165       //reset error, no reason to show it if there is no repositories
166       myNativeGitError.set(null);
167       return;
168     }
169     if (!isNativeGitInstalled()) {
170       LOG.info("Cannot find native git, skip running git gc");
171       return;
172     }
173     Long freeDiskSpace = FileUtil.getFreeSpace(myRepositoryManager.getBaseMirrorsDir());
174     LOG.info("Use git at path '" + myConfig.getPathToGit() + "'");
175     Collections.shuffle(allDirs);
176     int runGCCounter = 0;
177     LOG.info("Git garbage collection started");
178     boolean runInPlace = myConfig.runInPlaceGc();
179     for (File gitDir : allDirs) {
180       String url = myRepositoryManager.getUrl(gitDir.getName());
181       if (url != null) {
182         LOG.info("[" + gitDir.getName() + "] repository url: '" + url + "'");
183       }
184       if (enoughDiskSpaceForGC(gitDir, freeDiskSpace)) {
185         if (runInPlace) {
186           synchronized (myRepositoryManager.getWriteLock(gitDir)) {
187             runNativeGC(gitDir);
188           }
189         } else {
190           runGcInCopy(gitDir);
191         }
192       } else {
193         myGcErrors.registerError(gitDir, "Not enough disk space to run git gc");
194         LOG.warn("[" + gitDir.getName() + "] not enough disk space to run git gc (" + String.valueOf(freeDiskSpace) + " " + pluralize("byte", freeDiskSpace) + ")");
195       }
196       runGCCounter++;
197       final long repositoryFinishNanos = System.nanoTime();
198       if ((repositoryFinishNanos - startNanos) > gcTimeQuotaNanos) {
199         final int restRepositories = allDirs.size() - runGCCounter;
200         if (restRepositories > 0) {
201           LOG.info("Git garbage collection quota exceeded, skip " + restRepositories + " repositories");
202           break;
203         }
204       }
205     }
206     final long finishNanos = System.nanoTime();
207     LOG.info("Git garbage collection finished, it took " + TimeUnit.NANOSECONDS.toMillis(finishNanos - startNanos) + "ms");
208   }
209
210
211   private void runGcInCopy(@NotNull File originalRepo) {
212     Lock rmLock = myRepositoryManager.getRmLock(originalRepo).readLock();
213     rmLock.lock();
214     File gcRepo;
215     try {
216       if (!isGcNeeded(originalRepo)) {
217         LOG.info("[" + originalRepo.getName() + "] no git gc is needed");
218         myGcErrors.clearError(originalRepo);
219         return;
220       }
221
222       try {
223         gcRepo = setupGcRepo(originalRepo);
224       } catch (Exception e) {
225         myGcErrors.registerError(originalRepo, "Failed to create temporary repository for garbage collection", e);
226         LOG.warnAndDebugDetails("Failed to create temporary repository for garbage collection, original repository: " + originalRepo.getAbsolutePath(), e);
227         return;
228       }
229
230       LOG.info("[" + originalRepo.getName() + "] run git gc in dedicated dir [" + gcRepo.getName() + "]");
231
232       try {
233         repack(gcRepo);
234         packRefs(gcRepo);
235       } catch (Exception e) {
236         myGcErrors.registerError(originalRepo, "Error while running garbage collection", e);
237         LOG.warnAndDebugDetails("Error while running garbage collection in " + originalRepo.getAbsolutePath(), e);
238         FileUtil.delete(gcRepo);
239         return;
240       }
241     } finally {
242       rmLock.unlock();
243     }
244
245     //remove alternates pointing to the original repo before swapping repositories
246     FileUtil.delete(new File(gcRepo, "objects/info/alternates"));
247
248     long swapStart = System.currentTimeMillis();
249     File oldDir;
250     try {
251       oldDir = createTempDir(originalRepo.getParentFile(), originalRepo.getName() + ".old");
252       FileUtil.delete(oldDir);
253     } catch (Exception e) {
254       myGcErrors.registerError(originalRepo, "Error while creating temporary directory", e);
255       LOG.warnAndDebugDetails("Error while creating temporary directory for " + originalRepo.getAbsolutePath(), e);
256       FileUtil.delete(gcRepo);
257       return;
258     }
259
260     //swap repositories with write rm lock which guarantees no one uses the original repository
261     Lock rmWriteLock = myRepositoryManager.getRmLock(originalRepo).writeLock();
262     long lockStart = System.currentTimeMillis();
263     rmWriteLock.lock();
264     long lockDuration = System.currentTimeMillis() - lockStart;
265     try {
266       if (!renameDir(originalRepo, oldDir, 5)) {
267         myGcErrors.registerError(originalRepo, "Failed to rename " + originalRepo.getName() + " to " + oldDir.getName());
268         LOG.warn("Failed to rename " + originalRepo.getName() + " to " + oldDir.getName() + " after several attempts");
269         return;
270       }
271       if (!renameDir(gcRepo, originalRepo, 5)) {
272         myGcErrors.registerError(originalRepo, "Failed to rename " + gcRepo.getName() + " to " + originalRepo.getName());
273         LOG.warn("Failed to rename " + gcRepo.getName() + " to " + originalRepo.getName() + " after several attempts, will try restoring old repository");
274         if (!oldDir.renameTo(originalRepo)) {
275           LOG.warn("Failed to rename " + oldDir.getName() + " to " + originalRepo.getName());
276         }
277         return;
278       }
279     } finally {
280       rmWriteLock.unlock();
281       FileUtil.delete(oldDir);
282       FileUtil.delete(gcRepo);
283     }
284     long swapDuration = System.currentTimeMillis() - swapStart;
285     if (swapDuration > TimeUnit.SECONDS.toMillis(5)) {
286       String msg = "[" + originalRepo.getName() + "] swap with compacted repository finished in " + swapDuration + "ms";
287       if (lockDuration > TimeUnit.SECONDS.toMillis(1)) {
288         msg += " (lock acquired in " + lockDuration + "ms)";
289       }
290       LOG.info(msg);
291     }
292     myGcErrors.clearError(originalRepo);
293   }
294
295   @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "SameParameterValue"})
296   private boolean renameDir(@NotNull File prevDir, @NotNull File newDir, int numAttempts) {
297     try {
298       for (int i=0; i<numAttempts; i++) {
299         if (prevDir.renameTo(newDir)) return true;
300         Thread.sleep(100);
301       }
302     } catch (InterruptedException e) {
303       LOG.warn("Could not rename directory " + prevDir.getAbsolutePath() + " to " + newDir.getAbsolutePath() + ", operation was interrupted");
304     }
305     return false;
306   }
307
308   private void repack(final File gcRepo) throws VcsException {
309     long start = System.currentTimeMillis();
310     GeneralCommandLine cmd = new GeneralCommandLine();
311     cmd.setWorkingDirectory(gcRepo);
312     cmd.setExePath(myConfig.getPathToGit());
313     cmd.addParameter("repack");
314     cmd.addParameters("-a", "-d");
315     ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
316       @Override
317       public Integer getOutputIdleSecondsTimeout() {
318         return myConfig.getRepackIdleTimeoutSeconds();
319       }
320       @Override
321       public void onProcessFinished(@NotNull final Process ps) {
322         LOG.info("[" + gcRepo.getName() + "] 'git repack -a -d' finished in " + (System.currentTimeMillis() - start) + "ms");
323       }
324     });
325     VcsException commandError = CommandLineUtil.getCommandLineError("git repack", result);
326     if (commandError != null) {
327       LOG.warnAndDebugDetails("Error while running 'git repack' in " + gcRepo.getAbsolutePath(), commandError);
328       throw commandError;
329     }
330   }
331
332   private void packRefs(@NotNull File gcRepo) throws VcsException {
333     long start = System.currentTimeMillis();
334     GeneralCommandLine cmd = new GeneralCommandLine();
335     cmd.setWorkingDirectory(gcRepo);
336     cmd.setExePath(myConfig.getPathToGit());
337     cmd.addParameter("pack-refs");
338     cmd.addParameters("--all");
339     ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
340       @Override
341       public Integer getOutputIdleSecondsTimeout() {
342         return myConfig.getPackRefsIdleTimeoutSeconds();
343       }
344       @Override
345       public void onProcessFinished(@NotNull final Process ps) {
346         LOG.info("[" + gcRepo.getName() + "] 'git pack-refs --all' finished in " + (System.currentTimeMillis() - start) + "ms");
347       }
348     });
349     VcsException commandError = CommandLineUtil.getCommandLineError("git pack-refs", result);
350     if (commandError != null) {
351       LOG.warnAndDebugDetails("Error while running 'git pack-refs' in " + gcRepo.getAbsolutePath(), commandError);
352       throw commandError;
353     }
354   }
355
356   private boolean isGcNeeded(@NotNull File gitDir) {
357     FileRepository db = null;
358     try {
359       //implement logic from git gc --auto, jgit version we use doesn't have it yet
360       //and native git doesn't provide a dedicated command for that
361       db = (FileRepository) new RepositoryBuilder().setBare().setGitDir(gitDir).build();
362       return tooManyPacks(db) || tooManyLooseObjects(db);
363     } catch (IOException e) {
364       LOG.warnAndDebugDetails("Error while checking if garbage collection is needed in " + gitDir.getAbsolutePath(), e);
365       return false;
366     } finally {
367       if (db != null)
368         db.close();
369     }
370   }
371
372   private boolean enoughDiskSpaceForGC(@NotNull File gitDir, @Nullable Long freeDiskSpace) {
373     if (freeDiskSpace == null)
374       return true;
375     File objects = new File(gitDir, "objects");
376     File pack = new File(objects, "pack");
377     return FileUtil.getTotalDirectorySize(pack) < freeDiskSpace;
378   }
379
380   private boolean tooManyPacks(@NotNull FileRepository repo) {
381     int limit = repo.getConfig().getInt("gc", "autopacklimit", 50);
382     if (limit <= 0)
383       return false;
384     int packCount = 0;
385     for (PackFile packFile : repo.getObjectDatabase().getPacks()) {
386       if (!packFile.shouldBeKept())
387         packCount++;
388       if (packCount > limit)
389         return true;
390     }
391     return false;
392   }
393
394
395   private boolean tooManyLooseObjects(@NotNull FileRepository repo) {
396     int limit = repo.getConfig().getInt("gc", "auto", 6700);
397     if (limit <= 0)
398       return false;
399     //SHA is evenly distributed, we can estimate number of loose object by counting them in a single bucket (from jgit internals)
400     int bucketLimit = (limit + 255) / 256;
401     File bucket = new File(repo.getObjectsDirectory(), "17");
402     if (!bucket.isDirectory())
403       return false;
404     String[] files = bucket.list();
405     if (files == null)
406       return false;
407     int count = 0;
408     for (String fileName : files) {
409       if (PATTERN_LOOSE_OBJECT.matcher(fileName).matches())
410         count++;
411       if (count > bucketLimit)
412         return true;
413     }
414     return false;
415   }
416
417
418   @NotNull
419   private File setupGcRepo(@NotNull File gitDir) throws IOException {
420     File result = createTempDir(gitDir.getParentFile(), gitDir.getName() + ".gc");
421     Repository repo = new RepositoryBuilder().setBare().setGitDir(result).build();
422     try {
423       repo.create(true);
424     } finally {
425       repo.close();
426     }
427
428     //setup alternates, 'git repack' in a repo with alternates creates a pack
429     //in this repo without affecting the repo alternates point to
430     File objectsDir = new File(result, "objects");
431     File objectsInfo = new File(objectsDir, "info");
432     objectsInfo.mkdirs();
433     FileUtil.writeFileAndReportErrors(new File(objectsInfo, "alternates"), new File(gitDir, "objects").getCanonicalPath());
434
435     copyIfExist(new File(gitDir, "packed-refs"), result);
436     copyIfExist(new File(gitDir, "timestamp"), result);
437     copyDirIfExist(new File(gitDir, "refs"), result);
438     copyDirIfExist(new File(gitDir, "monitoring"), result);
439     return result;
440   }
441
442   private void copyIfExist(@NotNull File srcFile, @NotNull File dstDir) throws IOException {
443     if (srcFile.exists())
444       FileUtil.copy(srcFile, new File(dstDir, srcFile.getName()));
445   }
446
447   private void copyDirIfExist(@NotNull File srcDir, @NotNull File dstDir) throws IOException {
448     if (srcDir.exists())
449       FileUtil.copyDir(srcDir, new File(dstDir, srcDir.getName()));
450   }
451
452   @NotNull
453   private File createTempDir(@NotNull final File parentDir, @NotNull String name) throws IOException {
454     File dir = new File(parentDir, name);
455     if (dir.mkdir())
456       return dir;
457
458     int suffix = 0;
459     while (true) {
460       suffix++;
461       String tmpDirName = name + suffix;
462       dir = new File(parentDir, tmpDirName);
463       if (dir.mkdir())
464         return dir;
465     }
466   }
467
468   private void runJGitGC() {
469     final long startNanos = System.nanoTime();
470     final long gcTimeQuotaNanos = TimeUnit.MINUTES.toNanos(myConfig.getNativeGCQuotaMinutes());
471     LOG.info("Git garbage collection started");
472     List<File> allDirs = getAllRepositoryDirs();
473     Collections.shuffle(allDirs);
474     int runGCCounter = 0;
475     Boolean nativeGitInstalled = null;
476     boolean enableNativeGitLogged = false;
477     for (File gitDir : allDirs) {
478       synchronized (myRepositoryManager.getWriteLock(gitDir)) {
479         try {
480           LOG.info("Start garbage collection in " + gitDir.getAbsolutePath());
481           long repositoryStartNanos = System.nanoTime();
482           runJGitGC(gitDir);
483           LOG.info("Garbage collection finished in " + gitDir.getAbsolutePath() + ", duration: " +
484                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - repositoryStartNanos) + "ms");
485         } catch (Exception e) {
486           LOG.warnAndDebugDetails("Error while running garbage collection in " + gitDir.getAbsolutePath(), e);
487           if ((System.nanoTime() - startNanos) < gcTimeQuotaNanos) { //if quota is not exceeded try running a native git
488             if (nativeGitInstalled == null) {
489               LOG.info("Check if native git is installed");
490               nativeGitInstalled = isNativeGitInstalled();
491             }
492             if (nativeGitInstalled) {
493               runNativeGC(gitDir);
494             } else {
495               if (!enableNativeGitLogged) {
496                 LOG.info("Cannot find a native git, please install it and provide a path to git in the 'teamcity.server.git.executable.path' internal property.");
497                 enableNativeGitLogged = true;
498               }
499             }
500           }
501         }
502       }
503       runGCCounter++;
504       final long repositoryFinishNanos = System.nanoTime();
505       if ((repositoryFinishNanos - startNanos) > gcTimeQuotaNanos) {
506         final int restRepositories = allDirs.size() - runGCCounter;
507         if (restRepositories > 0) {
508           LOG.info("Git garbage collection quota exceeded, skip " + restRepositories + " repositories");
509           break;
510         }
511       }
512     }
513     final long finishNanos = System.nanoTime();
514     LOG.info("Git garbage collection finished, it took " + TimeUnit.NANOSECONDS.toMillis(finishNanos - startNanos) + "ms");
515   }
516
517   private boolean isNativeGitInstalled() {
518     String pathToGit = myConfig.getPathToGit();
519     GeneralCommandLine cmd = new GeneralCommandLine();
520     cmd.setWorkingDirectory(myRepositoryManager.getBaseMirrorsDir());
521     cmd.setExePath(pathToGit);
522     cmd.addParameter("version");
523     ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null);
524     VcsException commandError = CommandLineUtil.getCommandLineError("git version", result);
525     if (commandError != null) {
526       myNativeGitError.set(new RunGitError(pathToGit, commandError));
527       LOG.warnAndDebugDetails("Failed to run git", commandError);
528       return false;
529     } else {
530       myNativeGitError.set(null);
531     }
532     return true;
533   }
534
535   private void runJGitGC(final File bareGitDir) throws IOException, VcsException {
536     GeneralCommandLine cmd = new GeneralCommandLine();
537     cmd.setWorkingDirectory(bareGitDir);
538     cmd.setExePath(myConfig.getFetchProcessJavaPath());
539     cmd.addParameters("-Xmx" + myConfig.getGcProcessMaxMemory(),
540                       "-cp", myConfig.getFetchClasspath(),
541                       GitGcProcess.class.getName(),
542                       bareGitDir.getCanonicalPath());
543     ExecResult result = SimpleCommandLineProcessRunner.runCommand(cmd, null, new SimpleCommandLineProcessRunner.RunCommandEventsAdapter() {
544       @Nullable
545       @Override
546       public Integer getOutputIdleSecondsTimeout() {
547         return 60 * myConfig.getNativeGCQuotaMinutes();
548       }
549     });
550     VcsException commandError = CommandLineUtil.getCommandLineError("git gc", result, false, true);
551     if (commandError != null)
552       throw commandError;
553   }
554
555   private void runNativeGC(final File bareGitDir) {
556     String pathToGit = myConfig.getPathToGit();
557     try {
558       final long start = System.currentTimeMillis();
559       GeneralCommandLine cl = new GeneralCommandLine();
560       cl.setWorkingDirectory(bareGitDir.getParentFile());
561       cl.setExePath(pathToGit);
562       cl.addParameter("--git-dir="+bareGitDir.getCanonicalPath());
563       cl.addParameter("gc");
564       cl.addParameter("--auto");
565       cl.addParameter("--quiet");
566
567       ExecResult result = SimpleCommandLineProcessRunner.runCommand(cl, null, new SimpleCommandLineProcessRunner.ProcessRunCallback() {
568         public void onProcessStarted(Process ps) {
569           LOG.info("Start 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'");
570         }
571         public void onProcessFinished(Process ps) {
572           final long finish = System.currentTimeMillis();
573           LOG.info("Finish 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc', duration: " + (finish - start) + "ms");
574         }
575         public Integer getOutputIdleSecondsTimeout() {
576           return 60 * myConfig.getNativeGCQuotaMinutes();
577         }
578         public Integer getMaxAcceptedOutputSize() {
579           return null;
580         }
581       });
582
583       VcsException commandError = CommandLineUtil.getCommandLineError("'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", result);
584       if (commandError != null) {
585         LOG.warnAndDebugDetails("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", commandError);
586       }
587       if (result.getStderr().length() > 0) {
588         LOG.debug("Output produced by 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'");
589         LOG.debug(result.getStderr());
590       }
591     } catch (Exception e) {
592       myGcErrors.registerError(bareGitDir, e);
593       LOG.warnAndDebugDetails("Error while running 'git --git-dir=" + bareGitDir.getAbsolutePath() + " gc'", e);
594     }
595   }
596
597
598   @Nullable
599   public RunGitError getNativeGitError() {
600     return myNativeGitError.get();
601   }
602
603   public static class RunGitError extends Pair<String, VcsException> {
604     public RunGitError(@NotNull String gitPath, @NotNull VcsException error) {
605       super(gitPath, error);
606     }
607
608     @NotNull
609     public String getGitPath() {
610       return first;
611     }
612
613     @NotNull
614     public VcsException getError() {
615       return second;
616     }
617   }
618
619   @NotNull
620   private String pluralize(@NotNull String base, long n) {
621     //StringUtil doesn't work with longs
622     if (n == 1) return base;
623     return StringUtil.pluralize(base);
624   }
625 }