diff --git a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Branch.java b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Branch.java new file mode 100644 index 0000000000000000000000000000000000000000..8fcf1d52cf824e29f020757746ce0118fa423970 --- /dev/null +++ b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Branch.java @@ -0,0 +1,59 @@ +package de.uni_passau.fim.seibt.gitwrapper.repo; + +import de.uni_passau.fim.seibt.gitwrapper.process.ProcessExecutor; + +import java.util.Optional; +import java.util.function.Function; +import java.util.logging.Logger; + +public class Branch extends Reference { + + private static final Logger LOG = Logger.getLogger(Branch.class.getCanonicalName()); + + Branch(Repository repo, String name) { + super(repo, name); + } + + /** + * Checks this branch out, and tries to pull changes on this current branch. + * + * @return true if the pull was successful + */ + public Boolean pull() { + if (!repo.checkout(this)) { + return false; + } + + Optional<ProcessExecutor.ExecRes> fetch = git.exec(repo.getDir(), "pull"); + Function<ProcessExecutor.ExecRes, Boolean> toBoolean = res -> { + boolean failed = git.failed(res); + + if (failed) { + LOG.warning(() -> String.format("Pull of %s failed.", this)); + } + + return !failed; + }; + + return fetch.map(toBoolean).orElse(false); + } + + /** + * Returns the id of the {@link Commit} this reference is currently pointing to. + * + * @return the id of the {@link Commit} under this branch + */ + @Override + public String getId() { + return repo.toHash(id).orElse(id); + } + + /** + * Reruns the branch name + * + * @return the branch name + */ + public String getName() { + return id; + } +} diff --git a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Commit.java b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Commit.java index f769b70a6ead0cad119f5569a4b82f463062cdeb..6cccf32f42364cb26a6bee94b2d163f0d29805bc 100644 --- a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Commit.java +++ b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Commit.java @@ -1,22 +1,16 @@ package de.uni_passau.fim.seibt.gitwrapper.repo; -import de.uni_passau.fim.seibt.gitwrapper.process.ProcessExecutor.ExecRes; - import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneId; -import java.util.*; -import java.util.function.Function; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * A {@link Commit} made in a {@link Repository}. */ -public class Commit { - +public class Commit extends Reference { private static final Logger LOG = Logger.getLogger(Commit.class.getCanonicalName()); // since there is no porcelain format for this command, this regex is might depend on the git version @@ -24,11 +18,6 @@ public class Commit { private static final Pattern AUTHOR_INFO = Pattern.compile("author (.*?)<(.*?)> (\\d+) ([+-]\\d{4})\\n"); private static final Pattern COMMITTER_INFO = Pattern.compile("committer (.*?)<(.*?)> (\\d+) ([+-]\\d{4})\\n"); - protected GitWrapper git; - protected Repository repo; - - private String id; - private String message; private String author; @@ -46,73 +35,7 @@ public class Commit { * @param id the ID of the {@link Commit} */ Commit(Repository repo, String id) { - this.git = repo.getGit(); - this.repo = repo; - this.id = id; - } - - /** - * Returns the parents of this {@link Commit}. - * - * @return the parent {@link Commit} objects - */ - public List<Commit> getParents() { - Optional<ExecRes> revList = git.exec(repo.getDir(), "rev-list", "--parents", "-n", "1", id); - Function<ExecRes, List<Commit>> toParentsList = res -> { - String[] ids = res.stdOut.split("\\s+"); - - LOG.fine(() -> String.format("Found %d parents for %s.", ids.length - 1, this)); - LOG.finer(() -> String.format("Commit id and parents are:%n%s", String.join(System.lineSeparator(), ids))); - - return Arrays.stream(ids).skip(1).map(repo::getCommitUnchecked).collect(Collectors.toList()); - }; - - return revList.map(toParentsList).orElse(Collections.emptyList()); - } - - /** - * Optionally returns the merge base for <code>this</code> and <code>other</code>. The <code>other</code> commit - * must be part of the same {@link Repository} this {@link Commit} is. - * - * @param other the other {@link Commit} - * @return the merge base or an empty {@link Optional} if there is no merge base or an exception occurred - */ - public Optional<Commit> getMergeBase(Commit other) { - - if (!repo.equals(other.repo)) { - LOG.warning(() -> { - String msg = "Failed to obtain a merge base for %s and %s as they are not from the same repository."; - return String.format(msg, this, other); - }); - - return Optional.empty(); - } - - Optional<ExecRes> mergeBase = git.exec(repo.getDir(), "merge-base", getId(), other.getId()); - Function<ExecRes, Commit> toCommit = res -> { - - if (git.failed(res)) { - LOG.warning(() -> String.format("Failed to obtain a merge base for %s and %s.", this, other)); - return null; - } - - Commit base = repo.getCommitUnchecked(res.stdOut); - - LOG.fine(() -> String.format("Commits %s and %s have the merge base %s.", this, other, base)); - - return base; - }; - - return mergeBase.map(toCommit); - } - - /** - * Returns the ID of this commit. - * - * @return the ID - */ - public String getId() { - return id; + super(repo, id); } /** @@ -286,28 +209,4 @@ public class Commit { message = result.substring(result.lastIndexOf("\n")); }); } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - Commit commit = (Commit) o; - return Objects.equals(repo, commit.repo) && Objects.equals(id, commit.id); - } - - @Override - public int hashCode() { - return Objects.hash(repo, id); - } - - @Override - public String toString() { - return String.valueOf(id); - } } diff --git a/src/de/uni_passau/fim/seibt/gitwrapper/repo/DummyCommit.java b/src/de/uni_passau/fim/seibt/gitwrapper/repo/DummyCommit.java index 347d0ecdff1c8a3f7db322b8c420dd1f2a594fc3..1b082256477fcd9bf98f918dcf6777d281c84cfa 100644 --- a/src/de/uni_passau/fim/seibt/gitwrapper/repo/DummyCommit.java +++ b/src/de/uni_passau/fim/seibt/gitwrapper/repo/DummyCommit.java @@ -59,7 +59,7 @@ public class DummyCommit extends Commit { } private void setterWarning() { - LOG.warning(() -> "Ignoring a setter call on the DummyCommit for " + repo); + LOG.finest(() -> "Ignoring a setter call on the DummyCommit for " + repo); // TODO gets called often while parsing blame lines, maybe change level } } diff --git a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Reference.java b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Reference.java new file mode 100644 index 0000000000000000000000000000000000000000..d44394d8c33fb3f493430158f5305b13a5531b85 --- /dev/null +++ b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Reference.java @@ -0,0 +1,132 @@ +package de.uni_passau.fim.seibt.gitwrapper.repo; + +import de.uni_passau.fim.seibt.gitwrapper.process.ProcessExecutor; + +import java.util.*; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public abstract class Reference { + + private static final Logger LOG = Logger.getLogger(Reference.class.getCanonicalName()); + + protected final String id; + protected final Repository repo; + protected final GitWrapper git; + + protected Reference(Repository repo, String id) { + this.id = id; + this.repo = repo; + this.git = repo.getGit(); + } + + public String getId() { + return id; + } + + /** + * Optionally returns the merge base for <code>this</code> and <code>other</code>. The <code>other</code> reference + * must be part of the same {@link Repository} this {@link Reference} is. + * + * @param other the other {@link Reference} + * @return the merge base or an empty {@link Optional} if there is no merge base or an exception occurred + */ + public Optional<Commit> getMergeBase(Reference other) { + if (!repo.equals(other.repo)) { + LOG.warning(() -> { + String msg = "Failed to obtain a merge base for %s and %s as they are not from the same repository."; + return String.format(msg, this, other); + }); + + return Optional.empty(); + } + + Optional<ProcessExecutor.ExecRes> mergeBase = git.exec(repo.getDir(), "merge-base", getId(), other.getId()); + Function<ProcessExecutor.ExecRes, Commit> toCommit = res -> { + + if (git.failed(res)) { + LOG.warning(() -> String.format("Failed to obtain a merge base for %s and %s.", this, other)); + return null; + } + + Commit base = repo.getCommitUnchecked(res.stdOut); + + LOG.fine(() -> String.format("Commits %s and %s have the merge base %s.", this, other, base)); + + return base; + }; + + return mergeBase.map(toCommit); + } + + /** + * Performs a checkout followed by a merge of <code>this</code> and <code>other</code>. <code>Other</code> must be + * part of the same {@link Repository} this {@link Reference} is. + * + * @param other the {@link Reference} to merge + * @return <code>false</code>, if the merge failed, or contains conflicts. + */ + public boolean merge(Reference other) { + if (!repo.checkout(this)) { + return false; + } + + Optional<ProcessExecutor.ExecRes> mergeBase = git.exec(repo.getDir(), "merge", "-n", "-q", other.getId()); + Function<ProcessExecutor.ExecRes, Boolean> toBoolean = res -> { + if (git.failed(res)) { + LOG.warning(() -> String.format("Failed to merge %s and %s.", this, other)); + return null; + } + + // if there is no conflict, quiet and no-stat produce no output + return res.stdOut.isEmpty(); + }; + + return mergeBase.map(toBoolean).orElse(false); + } + + + /** + * Returns the parents of this {@link Commit}. + * + * @return the parent {@link Commit} objects + */ + public List<Commit> getParents() { + Optional<ProcessExecutor.ExecRes> revList = git.exec(repo.getDir(), "rev-list", "--parents", "-n", "1", id); + Function<ProcessExecutor.ExecRes, List<Commit>> toParentsList = res -> { + String[] ids = res.stdOut.split("\\s+"); + + LOG.fine(() -> String.format("Found %d parents for %s.", ids.length - 1, this)); + LOG.finer(() -> String.format("Commit id and parents are:%n%s", String.join(System.lineSeparator(), ids))); + + return Arrays.stream(ids).skip(1).map(repo::getCommitUnchecked).collect(Collectors.toList()); + }; + + return revList.map(toParentsList).orElse(Collections.emptyList()); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Commit commit = (Commit) o; + return Objects.equals(repo, commit.repo) && Objects.equals(id, commit.id); + } + + @Override + public final int hashCode() { + return Objects.hash(repo, id); + } + + @Override + public String toString() { + return String.valueOf(id); + } +} diff --git a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Repository.java b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Repository.java index 468d97e4bfa05bb3bd0e89120be8cb73fd78f05d..731c74cfaa9d49e4f9c14471cd12eb026355c5da 100644 --- a/src/de/uni_passau/fim/seibt/gitwrapper/repo/Repository.java +++ b/src/de/uni_passau/fim/seibt/gitwrapper/repo/Repository.java @@ -48,6 +48,7 @@ public class Repository { private File dir; private Map<String, Commit> commits; + private Map<String, Branch> branches; /** * Constructs a new {@link Repository}. @@ -72,23 +73,24 @@ public class Repository { this.commits = new HashMap<>(); this.commits.put(DummyCommit.DUMMY_COMMIT_ID, new DummyCommit(this)); + this.branches = new HashMap<>(); } /** - * Performs a checkout of the given {@link Commit}. + * Performs a checkout of the given {@link Reference}. * - * @param c - * the {@link Commit} to checkout + * @param ref + * the {@link Reference} to checkout * @return whether the checkout was successful * @see <a href=https://git-scm.com/docs/git-checkout>git checkout</a> */ - public boolean checkout(Commit c) { - Optional<ExecRes> checkout = git.exec(dir, "checkout", c.getId()); + public boolean checkout(Reference ref) { + Optional<ExecRes> checkout = git.exec(dir, "checkout", ref.getId()); Function<ExecRes, Boolean> toBoolean = res -> { boolean failed = git.failed(res); if (failed) { - LOG.warning(() -> String.format("Checkout of %s failed.", c)); + LOG.warning(() -> String.format("Checkout of %s failed.", ref)); } return !failed; @@ -97,6 +99,30 @@ public class Repository { return checkout.map(toBoolean).orElse(false); } + /** + * Performs a checkout of the given {@link Reference}. All changes since last commit will be discarded. + * + * @param ref + * the {@link Reference} to checkout + * @return whether the checkout was successful + * @see <a href=https://git-scm.com/docs/git-checkout>git checkout</a> + */ + public boolean forceCheckout(Reference ref) { + Optional<ExecRes> checkout = git.exec(dir, "reset", "--hard"); + Function<ExecRes, Boolean> toBoolean = res -> { + boolean failed = git.failed(res); + + if (failed) { + LOG.warning(() -> String.format("Reset of %s failed.", ref)); + return false; + } + + return checkout(ref); + }; + + return checkout.map(toBoolean).orElse(false); + } + /** * Performs a fetch in this {@link Repository}. * @@ -187,7 +213,6 @@ public class Repository { * @return the {@link Commit} or an empty {@link Optional} if the ID is invalid or an exception occurs */ public Optional<Commit> getCommit(String id) { - if (commits.containsKey(id)) { return Optional.of(commits.get(id)); } @@ -204,7 +229,7 @@ public class Repository { * * @param id * the ID to check - * @return true iff the ID designates a commit + * @return true if the ID designates a commit */ private boolean isCommit(String id) { Optional<ExecRes> catFile = git.exec(dir, "cat-file", "-t", id); @@ -227,6 +252,46 @@ public class Repository { return catFile.map(toBoolean).orElse(false); } + public Optional<Branch> getBranch(String name) { + if (branches.containsKey(name)) { + return Optional.of(branches.get(name)); + } + + if (!isBranch(name)) { + return Optional.empty(); + } + + return Optional.of(branches.computeIfAbsent(name, theName -> new Branch(this, theName))); + } + + /** + * Determines whether the given name designates a branch. + * + * @param name + * the branch name to check + * @return true if the ID designates a commit + */ + private boolean isBranch(String name) { + Optional<ExecRes> catFile = git.exec(dir, "branch", "--list", "--no-color", "--column=plain"); + Function<ExecRes, Boolean> toBoolean = res -> { + + if (git.failed(res)) { + LOG.warning(() -> String.format("Failed to determine whether %s is a valid branch id.", name)); + return null; + } + + boolean isBranch = res.stdOut.contains(name); + + if (isBranch) { + LOG.finer(() -> String.format("%s is a branch.", name)); + } + + return isBranch; + }; + + return catFile.map(toBoolean).orElse(false); + } + /** * Resolves the given <code>id</code> to the full SHA1 hash. * @@ -234,7 +299,7 @@ public class Repository { * the <code>id</code> to transform * @return the full SHA1 hash or an empty {@link Optional} if an exception occurs */ - private Optional<String> toHash(String id) { + Optional<String> toHash(String id) { Optional<ExecRes> revParse = git.exec(dir, "rev-parse", id); Function<ExecRes, String> toHash = res -> { @@ -260,7 +325,6 @@ public class Repository { * @see FileUtils#copyDirectory(File, File) */ public Optional<Repository> copy(File destination) { - try { if (destination.exists() && destination.isDirectory()) { LOG.warning(() -> String.format("%s already exists. Merging source and destination directories.", destination)); @@ -388,9 +452,11 @@ public class Repository { other.put(headerKey, lineScanner.nextLine().trim()); } } + if (authorInstant != null && authorTZ != null) { commit.setAuthorTime(OffsetDateTime.ofInstant(authorInstant, authorTZ)); } + if (committerInstant != null && committerTZ != null) { commit.setCommitterTime(OffsetDateTime.ofInstant(committerInstant, committerTZ)); }