package br.gov.servicos.editor.git; import br.gov.servicos.editor.conteudo.ListaDeConteudo; import br.gov.servicos.editor.security.UserProfile; import br.gov.servicos.editor.utils.LogstashProgressMonitor; import lombok.SneakyThrows; import lombok.experimental.FieldDefaults; import lombok.experimental.NonFinal; import lombok.extern.slf4j.Slf4j; import net.logstash.logback.marker.LogstashMarker; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.PullResult; import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.TrackingRefUpdate; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.slf4j.Marker; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; import static br.gov.servicos.editor.utils.Unchecked.Consumer.uncheckedConsumer; import static br.gov.servicos.editor.utils.Unchecked.Function.uncheckedFunction; import static java.util.Collections.singletonList; import static java.util.Optional.empty; import static java.util.Optional.of; import static java.util.stream.Collectors.toList; import static lombok.AccessLevel.PRIVATE; import static net.logstash.logback.marker.Markers.append; import static org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.NOTRACK; import static org.eclipse.jgit.api.ListBranchCommand.ListMode.ALL; import static org.eclipse.jgit.api.ListBranchCommand.ListMode.REMOTE; import static org.eclipse.jgit.api.RebaseCommand.Operation.ABORT; import static org.eclipse.jgit.api.ResetCommand.ResetType.HARD; import static org.eclipse.jgit.lib.ConfigConstants.*; import static org.eclipse.jgit.lib.Constants.*; import static org.eclipse.jgit.merge.MergeStrategy.THEIRS; @Slf4j @Service @FieldDefaults(level = PRIVATE, makeFinal = true) public class RepositorioGit { @NonFinal private String branchAtual; File raiz; boolean fazerPush; @NonFinal Git git; // disponível enquanto o repositório estiver aberto @Autowired public RepositorioGit(RepositorioConfig config) { raiz = config.localRepositorioDeCartas; fazerPush = config.fazerPush; } public Path getCaminhoAbsoluto() { return raiz.getAbsoluteFile().toPath(); } public Optional<Revisao> getRevisaoMaisRecenteDoBranch(String branchRef, Path caminhoRelativo) { RevCommit commit = comRepositorioAberto(uncheckedFunction(git -> { Repository repo = git.getRepository(); RevWalk revWalk = new RevWalk(repo); revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(caminhoRelativo.toString()), TreeFilter.ANY_DIFF)); revWalk.markStart(revWalk.lookupCommit(repo.resolve(branchRef))); Iterator<RevCommit> revs = revWalk.iterator(); if (revs.hasNext()) { return revs.next(); } return null; })); if (commit == null) { return empty(); } return of(new Revisao(commit)); } @SneakyThrows private <T> T comRepositorioAberto(Function<Git, T> fn) { try (Git git = Git.open(raiz)) { synchronized (RepositorioGit.class) { try { this.git = git; return fn.apply(git); } finally { this.git = null; } } } } @SneakyThrows public <T> T comRepositorioAbertoNoBranch(String branch, Supplier<T> supplier) { return comRepositorioAberto(git -> { checkout(branch); try { return supplier.get(); } finally { checkoutMaster(); } }); } @SneakyThrows private void checkoutMaster() { if ((R_HEADS + MASTER).equals(branchAtual)) { log.info("ignored: git checkout master"); return; } try { Ref result = git.checkout() .setName(R_HEADS + MASTER) .call(); Marker marker = append("git.branch", git.getRepository().getBranch()) .and(append("git.state", git.getRepository().getRepositoryState().toString())) .and(append("checkout.to", MASTER)) .and(append("checkout.result", result.getName())); log.info(marker, "git checkout master"); branchAtual = "master"; } catch (Exception e) { branchAtual = null; throw e; } } @SneakyThrows public boolean existeBranch(String id) { return comRepositorioAberto(uncheckedFunction(git -> { pull(); String idLimpo = id.replaceAll('^' + R_HEADS, ""); List<Ref> branchesList; if (ListaDeConteudo.CacheEsquentando.get()) { branchesList = git.branchList().call(); } else { branchesList = git.branchList().setListMode(ALL).call(); } Stream<String> branches = branchesList.stream().map(Ref::getName).map(n -> n.replaceAll(R_HEADS + '|' + R_REMOTES + "origin/", "")); return branches.anyMatch(s -> s.equals(idLimpo)); })); } @SneakyThrows private void checkout(String branch) { if (branch.equals(branchAtual)) { return; } try { Repository repository = git.getRepository(); String novoBranch = branch.replaceAll('^' + R_HEADS, ""); String branchRemoto = DEFAULT_REMOTE_NAME + '/' + novoBranch; LogstashMarker marker = append("git.branch", repository.getBranch()) .and(append("git.state", repository.getRepositoryState().toString())) .and(append("checkout.to", novoBranch)); if (repository.getRef(novoBranch) == null) { List<Ref> remoteBranches; if (ListaDeConteudo.CacheEsquentando.get()) { remoteBranches = singletonList(repository.getRef(branchRemoto)); } else { remoteBranches = git.branchList() .setListMode(REMOTE) .call(); } if (remoteBranches.contains(repository.getRef(branchRemoto))) { checkoutNovoBranch(novoBranch, branchRemoto); } else { checkoutNovoBranch(novoBranch, R_HEADS + MASTER); push(novoBranch); } criarTrackComBranchRemoto(novoBranch); marker = marker.and(append("checkout.branch.created", true)); } Ref result = git.checkout() .setName(novoBranch) .call(); marker = marker.and(append("checkout.result", result.getName())); log.info(marker, "git checkout {}", novoBranch); branchAtual = branch; } catch (Exception e) { branchAtual = null; throw e; } } private void checkoutNovoBranch(String novoBranch, String pontoDeInicio) throws GitAPIException, IOException { try { Ref result = git.branchCreate() .setName(novoBranch) .setStartPoint(pontoDeInicio) .setUpstreamMode(NOTRACK) .call(); Marker info = append("git.branch", git.getRepository().getBranch()) .and(append("git.state", git.getRepository().getRepositoryState().toString())) .and(append("branch.name", novoBranch)) .and(append("branch.start", R_HEADS + MASTER)) .and(append("branch.result", result.getName())); log.info(info, "git branch {}", novoBranch); branchAtual = novoBranch; } catch (Exception e) { branchAtual = null; throw e; } } @SneakyThrows public void add(Path caminho) { Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("git.branch", git.getRepository().getBranch())); log.debug(marker, "git add: {}", git.getRepository().getBranch(), caminho); git.add() .addFilepattern(caminho.toString()) .call(); } @SneakyThrows public void commit(Path caminho, String mensagem, UserProfile profile) { PersonIdent ident = new PersonIdent(profile.getName(), profile.getEmail()); try { RevCommit result = git.commit() .setMessage(mensagem) .setCommitter(ident) .setAuthor(ident) .setOnly(caminho.toString()) .call(); Marker marker = append("commit", result.getName()) .and(append("commit.message", mensagem)) .and(append("commit.author", ident.getName())) .and(append("commit.email", ident.getEmailAddress())) .and(append("commit.path", caminho.toString())) .and(append("git.branch", git.getRepository().getBranch())) .and(append("git.state", git.getRepository().getRepositoryState().toString())); log.info(marker, "git commit {}", caminho); } catch (JGitInternalException e) { if (e.getMessage().equals(JGitText.get().emptyCommit)) { log.info("Commit não possui alterações em {}", caminho); } else { throw e; } } } public void pull() { if (ListaDeConteudo.CacheEsquentando.get()) { log.debug("Cache esquentando - ignorando pull()"); return; } try { PullResult result = git.pull() .setRebase(true) .setProgressMonitor(new LogstashProgressMonitor(log)) .call(); Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("git.branch", git.getRepository().getBranch())) .and(append("pull.fetched.from", result.getFetchedFrom())) .and(append("pull.fetch.result.updates", result.getFetchResult() == null ? null : result.getFetchResult().getMessages())) .and(append("pull.fetch.result.updates", result.getFetchResult() == null ? null : result.getFetchResult().getTrackingRefUpdates().stream().map(TrackingRefUpdate::getResult).map(Enum::toString).collect(toList()))) .and(append("pull.rebase.result", result.getRebaseResult() == null ? null : result.getRebaseResult().getStatus().toString())) .and(append("pull.merge.result", result.getMergeResult() == null ? null : result.getMergeResult().getMergeStatus().toString())); log.info(marker, "git pull em {}", git.getRepository().getBranch()); if (!result.isSuccessful()) { colocarEmSAFE(); throw new RepositorioEstadoInvalidoException("Não foi possível completar o git pull"); } } catch (WrongRepositoryStateException e) { // o repositório pode entrar em estado inválido, e neste caso fazemos um rebase e voltamos o repositório para um estado válido try { colocarEmSAFE(); } finally { throw new RepositorioEstadoInvalidoException("Não foi possível completar o git pull", e); } } catch (IOException | GitAPIException e) { throw new RuntimeException(e); } } private void colocarEmSAFE() throws GitAPIException, IOException { RebaseResult rebaseResult = git .rebase() .setStrategy(THEIRS) .setOperation(ABORT) .call(); Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("git.branch", git.getRepository().getBranch())) .and(append("pull.rebase.result", rebaseResult.getStatus().toString())); log.info(marker, "git rebase em {}", git.getRepository().getBranch()); Ref resetResult = git.reset().setMode(HARD).setRef(R_HEADS + MASTER).call(); marker = append("reset.result", resetResult.getName()); log.info(marker, "git reset --hard em {}", git.getRepository().getBranch()); } public void push(String branch) { pushBranch(branch, branch); } private void pushBranch(String branchLocal, String branchRemoto) { if (!fazerPush) { log.info("Envio de alterações ao Github desligado (FLAGS_GIT_PUSH=false)"); return; } if (ListaDeConteudo.CacheEsquentando.get()) { log.debug("Cache esquentando - ignorando push()"); return; } try { git.push() .setRemote(DEFAULT_REMOTE_NAME) .setRefSpecs(new RefSpec(branchLocal + ':' + branchRemoto)) .setProgressMonitor(new LogstashProgressMonitor(log)) .call() .forEach(uncheckedConsumer(result -> { Marker marker = append("push.messages", result.getMessages()) .and(append("push.updates", result.getRemoteUpdates().stream().map(u -> u.getStatus().toString()).collect(toList()))) .and(append("git.branch", git.getRepository().getBranch())) .and(append("git.state", git.getRepository().getRepositoryState().toString())); log.info(marker, "git push em {}", branchRemoto); })); } catch (GitAPIException e) { log.error(append("push.branch", branchRemoto), "git push falhou", e); e.printStackTrace(); } } @SneakyThrows public void deleteLocalBranch(String branch) { git.branchDelete() .setForce(true) .setBranchNames(branch) .call(); Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("branch.delete", branch)); log.info(marker, "git branch delete {}", branch); } public void deleteRemoteBranch(String branch) { pushBranch("", branch); } @SneakyThrows public void remove(Path caminho) { git.rm() .addFilepattern(caminho.toString()) .call(); Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("git.branch", git.getRepository().getBranch())); log.debug(marker, "git rm {}", caminho); } public Stream<String> branches() { return comRepositorioAberto(git -> { LogstashMarker marker = append("git.state", git.getRepository().getRepositoryState().toString()); try { marker = marker.and(append("git.branch", git.getRepository().getBranch())); List<Ref> branches = git.branchList().call(); log.info(marker, "git branch list: {} branches", branches.size()); return branches.stream() .map(Ref::getName) .map(n -> n.replaceAll(R_HEADS, "")); } catch (IOException | GitAPIException e) { log.error(marker, "Erro ao listar branches", e); return Stream.<String>empty(); } }); } @SneakyThrows public void moveBranchPara(String novoBranch) { String antigo = git.getRepository().getBranch(); git.branchRename().setNewName(novoBranch).call(); criarTrackComBranchRemoto(novoBranch); Marker marker = append("git.state", git.getRepository().getRepositoryState().toString()) .and(append("branch.rename.old", antigo)) .and(append("branch.rename.new", git.getRepository().getBranch())); log.info(marker, "git branch move {}", novoBranch); } private void criarTrackComBranchRemoto(String novoBranch) throws IOException { StoredConfig config = git.getRepository().getConfig(); config.setString(CONFIG_BRANCH_SECTION, novoBranch, CONFIG_KEY_REMOTE, DEFAULT_REMOTE_NAME); config.setString(CONFIG_BRANCH_SECTION, novoBranch, CONFIG_KEY_MERGE, R_HEADS + novoBranch); config.save(); } }