Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.telenav.cactus.maven.git.GitCheckout Maven / Gradle / Ivy
package com.telenav.cactus.maven.git;
import com.mastfrog.util.preconditions.Exceptions;
import com.telenav.cactus.maven.log.BuildLog;
import com.telenav.cactus.maven.git.Branches.Branch;
import com.telenav.cactus.maven.util.PathUtils;
import com.telenav.cactus.maven.util.ProcessResultConverter;
import com.telenav.cactus.maven.util.ThrowingOptional;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
*
*
* @author Tim Boudreau
*/
public final class GitCheckout implements Comparable
{
private static final DateTimeFormatter GIT_LOG_FORMAT = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4).appendLiteral("-")
.appendValue(ChronoField.MONTH_OF_YEAR, 2).appendLiteral("-")
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.appendLiteral(' ')
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.appendLiteral(' ')
.appendOffset("+HHMM", "+0000")
.parseLenient()
.toFormatter();
public static final DateTimeFormatter ISO_INSTANT = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendInstant()
.toFormatter(Locale.US);
public static final GitCommand GET_BRANCH
= new GitCommand<>(ProcessResultConverter.strings().trimmed(),
"rev-parse", "--abbrev-ref", "HEAD");
public static final GitCommand GET_HEAD
= new GitCommand<>(ProcessResultConverter.strings().trimmed(),
"rev-parse", "HEAD");
public static final GitCommand UPDATE_REMOTE_HEADS
= new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
"remote", "update");
public static final GitCommand NO_MODIFICATIONS
= new GitCommand<>(ProcessResultConverter.strings().trimmed().trueIfEmpty(),
"status", "--porcelain");
public static final GitCommand> LIST_REMOTES
= new GitCommand<>(ProcessResultConverter.strings().trimmed().map(GitRemotes::from),
"remote", "-v");
public static final GitCommand ALL_BRANCHES
= new GitCommand<>(ProcessResultConverter.strings().trimmed().map(Branches::from),
"branch", "--no-color", "-a");
public static final GitCommand REMOTE_HEADS
= new GitCommand<>(ProcessResultConverter.strings().trimmed().map(Heads::from),
"ls-remote");
public static final GitCommand IS_DIRTY
= new GitCommand<>(ProcessResultConverter.exitCode(code -> code != 0),
"diff", "--quiet");
public static final GitCommand ADD_CHANGED
= new GitCommand<>(ProcessResultConverter.strings(),
"add", "--all");
public static final GitCommand PULL
= new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
"pull");
public static final GitCommand PUSH
= new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
"push");
public static final GitCommand IS_DETACHED_HEAD
= new GitCommand<>(ProcessResultConverter.strings().testedWith(
text -> text.contains("(detached)")),
"status", "--porcelain=2", "--branch");
private final GitCommand> commitDate
= new GitCommand<>(ProcessResultConverter.strings().trimmed()
.map(this::fromGitLogFormat),
"--no-pager", "log", "-1", "--format=format:%cd",
"--date=iso", "--no-color", "--encoding=utf8");
private final GitCommand> listSubmodules
= new GitCommand<>(ProcessResultConverter.strings().trimmed()
.map(this::parseSubmoduleInfo), "submodule", "status");
private static final ZoneId GMT = ZoneId.of("GMT");
private final BuildLog log = BuildLog.get();
private final Path root;
GitCheckout(Path root)
{
this.root = root;
}
private Optional fromGitLogFormat(String txt)
{
if (txt.isEmpty()) {
log.error("Got an empty timestamp from git log for " + root
+ " branch " + branch() + " head " + head());
return Optional.empty();
}
try {
return Optional.of(
ZonedDateTime.parse(txt, GIT_LOG_FORMAT)
.withZoneSameInstant(GMT));
} catch (DateTimeParseException ex) {
log.error("Failed to parse git log date string '" + txt + "'", ex);
return Optional.empty();
}
}
public Branches branches()
{
return ALL_BRANCHES.withWorkingDir(root).run().awaitQuietly();
}
public static Optional repository(File dir)
{
return repository(dir.toPath());
}
public static Optional repository(Path dirOrFile)
{
return PathUtils.findGitCheckoutRoot(dirOrFile, false)
.map(GitCheckout::new);
}
public static Optional submodulesRoot(Path dirOrFile)
{
return PathUtils.findGitCheckoutRoot(dirOrFile, false)
.map(GitCheckout::new);
}
public Optional remote(String name)
{
return Optional.ofNullable(LIST_REMOTES.withWorkingDir(root).run().awaitQuietly().get(name));
}
public Optional defaultRemote()
{
Collection extends GitRemotes> remotes = allRemotes();
if (remotes.isEmpty())
{
return Optional.empty();
}
for (GitRemotes rem : remotes)
{
if ("origin".equals(rem.name))
{
return Optional.of(rem);
}
}
return Optional.of(remotes.iterator().next());
}
/**
* The local directory name something is checked out into is not necessarily
* related in any way to the project name, so when filtering which projects
* we care about branch names in, treat the remote name as (at least
* somewhat more) authoritative.
*
* @return A set of strings that are the final components of all git remote
* urls configured for this repo, with any ".git" suffix trimmed
*/
public Set remoteProjectNames()
{
Set result = new HashSet<>();
allRemotes().forEach(remote -> remote.collectRemoteNames(result));
return result;
}
public Optional commitDate()
{
return commitDate.withWorkingDir(root).run().awaitQuietly();
}
public Collection extends GitRemotes> allRemotes()
{
return LIST_REMOTES.withWorkingDir(root).run().awaitQuietly().values();
}
public GitCheckout updateRemoteHeads()
{
UPDATE_REMOTE_HEADS.withWorkingDir(root).run().awaitQuietly();
return this;
}
public Optional branch()
{
String branch = GET_BRANCH.withWorkingDir(root).run().awaitQuietly();
switch (branch)
{
case "HEAD": // this is the output if we are in detached-head mode
return Optional.empty();
default:
return Optional.of(branch);
}
}
public Optional mergeBase()
{
Branches branches = branches();
return branches.currentBranch().flatMap(currentBranch ->
{
return branches.opposite(currentBranch).map(remoteBranch ->
{
return new GitCommand<>(ProcessResultConverter.strings().trimmed(),
root, "merge-base", "@", remoteBranch.trackingName()).run().awaitQuietly();
});
});
}
public boolean push()
{
return PUSH.withWorkingDir(root).run().awaitQuietly();
}
public Heads remoteHeads()
{
return REMOTE_HEADS.withWorkingDir(root).run().awaitQuietly();
}
public String name()
{
return submoduleRoot().map(sroot -> sroot.equals(this) ? "" : sroot.checkoutRoot().relativize(root).toString())
.orElse(root.getFileName().toString());
}
public boolean pushCreatingBranch()
{
Optional branch = branch();
if (!branch.isPresent())
{
return false;
}
Optional remote = this.defaultRemote();
if (!remote.isPresent())
{
return false;
}
String output = new GitCommand<>(ProcessResultConverter.strings(), root,
"push", "-u", remote.get().name, branch.get()).run().awaitQuietly();
log.info(output);
return true;
}
/**
* Determines if a push is needed, and whether or not the remote branch
* exists.
*
* @return A result
*/
public NeedPushResult needsPush()
{
String remote = defaultRemote().map(rem -> rem.name()).orElse("origin");
Branches branches = branches();
if (!branches.currentBranch().isPresent())
{
return NeedPushResult.NOT_ON_A_BRANCH;
}
Branch br = branches.currentBranch().get();
Optional remBranch = branches.opposite(br);
if (!remBranch.isPresent())
{
return NeedPushResult.REMOTE_BRANCH_DOES_NOT_EXIST;
}
boolean logWasEmpty = new GitCommand<>(ProcessResultConverter.strings().testedWith(String::isBlank),
root, "log", remote + "/" + remBranch.get().name() + ".." + br.name()).run().awaitQuietly();
return NeedPushResult.of(!logWasEmpty);
}
/**
* Switch to a local branch (which must already exist).
*
* @param localBranch A branch
* @return true if the command succeeds
*/
public boolean switchToBranch(String localBranch)
{
return new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
root, "checkout", localBranch).run().awaitQuietly();
}
/**
* Updates .gitmodules so someone grabbing this branch and running git pull
* in a submodule pulls from the correct remote branch.
*
* @param submodule The submodule - a path relative to the root
* @param branch The branch name
*/
public void setSubmoduleBranch(String submodule, String branch)
{
new GitCommand<>(ProcessResultConverter.strings(),
root, "submodule", "set-branch", "-b", branch, submodule).run().awaitQuietly();
}
public boolean pull()
{
return PULL.withWorkingDir(root).run().awaitQuietly();
}
/**
* Create a new branch (which must not yet exist) and switch to it. If a
* remote branch of the same name exists, will automatically be set up to
* track it.
*
* @param newLocalBranch The branch name to create
* @return true if the command succeeds
*/
public boolean createAndSwitchToBranch(String newLocalBranch, Optional fallbackTrackingBranch)
{
return createAndSwitchToBranch(newLocalBranch, fallbackTrackingBranch, false);
}
public boolean createAndSwitchToBranch(String newLocalBranch, Optional fallbackTrackingBranch, boolean pretend)
{
if (newLocalBranch.isEmpty())
{
throw new IllegalArgumentException("Empty new branch name");
}
Branches branches = branches();
if (branches.find(newLocalBranch, true).isPresent())
{
throw new IllegalArgumentException(
"Already contains a local branch named '"
+ newLocalBranch + "': " + this);
}
Optional remoteTrackingBranch = branches.find(newLocalBranch, false);
if (!remoteTrackingBranch.isPresent())
{
if (fallbackTrackingBranch.isPresent())
{
String fallback = fallbackTrackingBranch.get();
remoteTrackingBranch = branches.find(fallback, true);
if (!remoteTrackingBranch.isPresent())
{
remoteTrackingBranch = branches.find(fallback, false);
}
if (!remoteTrackingBranch.isPresent())
{
log.warn("Could not find a branch to track for '"
+ newLocalBranch
+ "' or a local or remote branch named '"
+ fallback + "'");
}
}
}
if (remoteTrackingBranch.isPresent())
{
String track = remoteTrackingBranch.get().trackingName();
if (pretend)
{
return true;
}
// GitCommand cmd = new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
// root,
// "checkout", "-b", newLocalBranch, "-t", track);
GitCommand cmd = new GitCommand<>(ProcessResultConverter.strings(),
root,
"checkout", "-b", newLocalBranch, "-t", track);
cmd.run().awaitQuietly();
return true;
} else
{
if (pretend)
{
return true;
}
GitCommand cmd = new GitCommand<>(ProcessResultConverter.exitCodeIsZero(),
root,
"checkout", "-b", newLocalBranch);
return cmd.run().awaitQuietly();
}
}
public boolean hasPomInRoot()
{
return Files.exists(root.resolve("pom.xml"));
}
public boolean addAll()
{
ADD_CHANGED.withWorkingDir(root).run().awaitQuietly();
return true;
}
public String head()
{
return GET_HEAD.withWorkingDir(root).run().awaitQuietly();
}
public boolean isInSyncWithRemoteHead()
{
Branches branches = branches();
return branches.currentBranch().map(branch ->
{
System.out.println(" sync-check " + root.getFileName() + " branch " + branch);
return branches.find(branch.name(), false).map(remoteBranch ->
{
String remoteHead = new GitCommand<>(ProcessResultConverter.strings().trimmed(), root, "rev-parse", remoteBranch.trackingName())
.run().awaitQuietly();
String head = head();
System.out.println(" remote-head " + remoteHead + " loc head " + head);
return remoteHead.equals(head);
}).orElse(false);
}).orElse(false);
}
public Optional remoteHead()
{
Branches branches = branches();
return branches.currentBranch().flatMap(branch ->
{
return branches.find(branch.name(), false).map(remoteBranch ->
{
return new GitCommand<>(ProcessResultConverter.strings().trimmed(),
root, "rev-parse", remoteBranch.trackingName())
.run().awaitQuietly();
});
});
}
public boolean add(Collection extends Path> paths)
{
List list = new ArrayList<>(Arrays.asList("add"));
for (Path path : paths)
{
if (path.isAbsolute())
{
path = root.relativize(path);
}
list.add(path.toString());
}
GitCommand cmd = new GitCommand<>(ProcessResultConverter.strings(),
root, list.toArray(String[]::new));
String output = cmd.run().awaitQuietly();
log.info(output);
return true;
}
public boolean commit(String message)
{
String commitOut = new GitCommand<>(ProcessResultConverter.strings(), root,
"commit", "-m", message).run().awaitQuietly();
log.info(commitOut);
return true;
}
public boolean hasUncommitedChanges()
{
return !NO_MODIFICATIONS.withWorkingDir(root).run().awaitQuietly();
}
public boolean isDirty()
{
return IS_DIRTY.withWorkingDir(root).run().awaitQuietly();
}
public boolean isDetachedHead()
{
return IS_DETACHED_HEAD.withWorkingDir(root).run().awaitQuietly();
}
public void scanForPomFiles(Consumer pomConsumer)
{
boolean isRoot = isSubmoduleRoot();
Predicate isSameRoot = path ->
{
return !"target".equals(path.getFileName().toString()) && Files.isDirectory(path)
&& (isRoot || (root.equals(path) || !Files.exists(path.resolve(".git"))));
};
if (hasPomInRoot())
{
pomConsumer.accept(root.resolve("pom.xml"));
}
try ( Stream str = Files.walk(root).filter(isSameRoot))
{
str.forEach(path ->
{
Path pom = path.resolve("pom.xml");
if (Files.exists(pom) && (path.equals(root) || !Files.exists(path.resolve(".git"))))
{
pomConsumer.accept(pom);
}
});
} catch (IOException ioe)
{
Exceptions.chuck(ioe);
}
}
public Set pomFiles(boolean fromRoot)
{
Set result = new HashSet<>();
if (fromRoot)
{
submoduleRoot().ifPresent(gitRoot ->
{
gitRoot.scanForPomFiles(result::add);
});
} else
{
scanForPomFiles(result::add);
}
return result;
}
public boolean isSubmoduleRoot()
{
if (Files.exists(root.resolve(".gitmodules")))
{
Optional submoduleRoot = PathUtils.findGitCheckoutRoot(root, true);
return submoduleRoot.isPresent() && root.equals(submoduleRoot.get());
}
return false;
}
public ThrowingOptional submoduleRoot()
{
return ThrowingOptional.from(PathUtils.findGitCheckoutRoot(root, true))
.map((Path dir) ->
{
if (dir.equals(root))
{
return this;
}
return new GitCheckout(dir);
});
}
public Path checkoutRoot()
{
return root;
}
@Override
public String toString()
{
return checkoutRoot().toString();
}
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
} else if (o == null || o.getClass() != GitCheckout.class)
{
return false;
}
return ((GitCheckout) o).checkoutRoot().equals(checkoutRoot());
}
@Override
public int hashCode()
{
return checkoutRoot().hashCode();
}
public static boolean isGitFlowInstalled()
{
return PathUtils.findExecutable("git-flow").isPresent();
}
public ThrowingOptional> submodules()
{
if (isSubmoduleRoot())
{
List infos = listSubmodules
.withWorkingDir(root)
.run()
.awaitQuietly();
return infos.isEmpty() ? ThrowingOptional.empty() : ThrowingOptional.of(infos);
} else
{
return submoduleRoot().flatMapThrowing(rootRepo -> rootRepo.submodules());
}
}
List parseSubmoduleInfo(String output)
{
return SubmoduleStatus.fromStatusOutput(root, output);
}
@Override
public int compareTo(GitCheckout o)
{
return root.compareTo(o.root);
}
public ThrowingOptional submoduleRelativePath()
{
if (isSubmoduleRoot())
{
return ThrowingOptional.empty();
}
return submoduleRoot().map(
rootCheckout -> rootCheckout.checkoutRoot().relativize(root));
}
}