Skip to content

Commit

Permalink
feat: jgit status
Browse files Browse the repository at this point in the history
fix: #100
  • Loading branch information
minh authored and nqminhuit committed Jul 14, 2024
1 parent ff556b9 commit c02ea08
Show file tree
Hide file tree
Showing 15 changed files with 324 additions and 48 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@

name: Java CI with Maven

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
on: [push, pull_request]

jobs:
build:
Expand Down
30 changes: 18 additions & 12 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<jar.plugin.version>3.3.0</jar.plugin.version>
<jacoco.version>0.8.12</jacoco.version>
<commit.id.version>9.0.1</commit.id.version>
<jgit.version>6.10.0.202406032230-r</jgit.version>
</properties>

<dependencies>
Expand All @@ -32,6 +33,23 @@
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${jgit.version}</version>
</dependency>

<!-- these 2 slf4j deps are required for jgit -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>

<!-- test dependencies -->
<dependency>
Expand Down Expand Up @@ -64,18 +82,6 @@
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/org/nqm/Gis.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package org.nqm;

import java.io.IOException;
import java.nio.file.Path;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.nqm.command.GisCommand;
import org.nqm.command.GisVersion;
import org.nqm.command.GitCommand;
import org.nqm.command.GitStatusCommand;
import org.nqm.config.GisLog;
import org.nqm.utils.GisProcessUtils;
import picocli.CommandLine;
Expand All @@ -16,7 +21,7 @@
description = "Git extension wrapper which supports submodules",
mixinStandardHelpOptions = true,
versionProvider = GisVersion.class)
public class Gis extends GitCommand {
public class Gis extends GisCommand {

private static final IExecutionExceptionHandler GLOBAL_EXCEPTION_HANLER = new IExecutionExceptionHandler() {
@Override
Expand All @@ -38,9 +43,14 @@ public static void setDryRun(boolean dryRun) {
}

public static void main(String... args) {
// try {
// System.out.println(GitStatusCommand.status(Path.of("/home/minh/projects/test/small-git-root-module/small-git-domain/.git"), false));
// } catch (NoWorkTreeException | IOException | GitAPIException e) {
// e.printStackTrace();
// }

var gis = new CommandLine(new Gis());
gis.setExecutionExceptionHandler(GLOBAL_EXCEPTION_HANLER);

gis.execute(args.length == 0
? new String[] {GIT_STATUS, "--one-line"}
: args);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/nqm/command/CommandVerticle.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.nqm.command;

import static org.nqm.command.GitCommand.HOOKS_OPTION;
import static org.nqm.command.GisCommand.HOOKS_OPTION;
import static org.nqm.utils.GisStringUtils.isNotBlank;
import static org.nqm.utils.StdOutUtils.gitStatus;
import static org.nqm.utils.StdOutUtils.gitStatusOneLine;
Expand Down Expand Up @@ -95,7 +95,7 @@ private static void safelyPrint(
var sb = new StringBuilder(infof("%s", "" + path.getFileName()));
var isOneLineOpt = Stream.of(gisOptions).anyMatch("--gis-one-line"::equals);
while (isNotBlank(line = input.readLine())) {
if (commandWithArgs[1].equals(GitCommand.GIT_STATUS)) {
if (commandWithArgs[1].equals(GisCommand.GIT_STATUS)) {
sb.append(isOneLineOpt ? gitStatusOneLine(line) : gitStatus(line));
} else {
sb.append("%n %s".formatted(line));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.nqm.command;

import static org.nqm.command.Wrapper.forEachModuleDo;
import static org.nqm.command.Wrapper.forEachModuleStatus;
import static org.nqm.command.Wrapper.forEachModuleWith;
import static org.nqm.config.GisConfig.currentDir;
import java.io.BufferedReader;
Expand All @@ -15,9 +16,13 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.nqm.GisException;
import org.nqm.config.GisConfig;
import org.nqm.config.GisLog;
Expand All @@ -28,7 +33,7 @@
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

public class GitCommand {
public class GisCommand {

private static final String CHECKOUT = "checkout";
private static final String FETCHED_AT = "(fetched at: %s)";
Expand All @@ -47,12 +52,18 @@ void pull() throws IOException {
}

@Command(name = GIT_STATUS, aliases = "st", description = "Show the working trees status")
void status(@Option(names = "--one-line") boolean oneLineOpt) throws IOException {
if (oneLineOpt) {
forEachModuleDo(GIT_STATUS, "-sb", "--ignore-submodules", "--porcelain=v2", "--gis-one-line");
} else {
forEachModuleDo(GIT_STATUS, "-sb", "--ignore-submodules", "--porcelain=v2");
}
void status(@Option(names = "--one-line") boolean oneLiner) throws IOException {
var result = new ConcurrentLinkedQueue<String>();
forEachModuleStatus(f -> {
try {
result.add(GitStatusCommand.status(f, oneLiner));
} catch (IOException | GitAPIException e) {
GisLog.debug(e);
}
});
StdOutUtils.println(String.join(
GisStringUtils.NEWLINE,
result.stream().collect(Collectors.toCollection(TreeSet::new))));
if (Files.exists(TMP_FILE)) {
var lastFetched = Files.readString(TMP_FILE);
if (GisStringUtils.isNotBlank(lastFetched)) {
Expand Down Expand Up @@ -235,7 +246,7 @@ void generateCompletion(
try (var out = new FileOutputStream(file.toFile())) {
while ((line = buffer.readLine()) != null) {
out.write(line.getBytes());
out.write("%n".formatted().getBytes());
out.write(GisStringUtils.NEWLINE.getBytes());
}
}
return;
Expand Down
139 changes: 139 additions & 0 deletions src/main/java/org/nqm/command/GitStatusCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.nqm.command;

import static org.nqm.model.FileStatus.ADDED;
import static org.nqm.model.FileStatus.CHANGED;
import static org.nqm.model.FileStatus.CONFLICT;
import static org.nqm.model.FileStatus.MISSING;
import static org.nqm.model.FileStatus.MODIFIED;
import static org.nqm.model.FileStatus.REMOVED;
import static org.nqm.model.FileStatus.UNTRACKED;
import static org.nqm.model.FileStatus.UNTRACKED_DIRS;
import static org.nqm.utils.GisStringUtils.getDirectoryName;
import static org.nqm.utils.StdOutUtils.CL_GREEN;
import static org.nqm.utils.StdOutUtils.CL_RED;
import static org.nqm.utils.StdOutUtils.coloringBranch;
import static org.nqm.utils.StdOutUtils.coloringWord;
import static org.nqm.utils.StdOutUtils.infof;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.lib.BranchConfig;
import org.eclipse.jgit.lib.BranchTrackingStatus;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode;
import org.nqm.model.FileStatus;
import org.nqm.utils.GisStringUtils;
import org.nqm.utils.StdOutUtils;

public class GitStatusCommand {

private static Repository openRepo(Path f) throws IOException {
var repo = new FileRepositoryBuilder().readEnvironment();
if (f != null) {
repo.findGitDir(f.toFile());
} else {
repo.findGitDir();
}
return repo.build();
}

private static String aheadBehind(Repository repo) throws IOException {
var trackingStatus = BranchTrackingStatus.of(repo, repo.getBranch());
int ahead = trackingStatus.getAheadCount();
int behind = trackingStatus.getBehindCount();
return StdOutUtils.buildAheadBehind(ahead, behind);
}

private static String statusOneLiner(Repository repo, Status status, String localBranch)
throws IOException {
// TODO: add styling modified and changed files
// TODO: add styling for upstream?
var result = new StringBuilder(" ")
.append(coloringBranch(localBranch))
.append(aheadBehind(repo));

var updatedFiles = Stream.of(status.getModified().stream(),
status.getChanged().stream(),
status.getAdded().stream(),
status.getMissing().stream(),
status.getRemoved().stream(),
status.getUntracked().stream(),
status.getConflicting().stream(),
status.getUntrackedFolders().stream())
.reduce(Stream::concat)
.orElseGet(Stream::empty)
.distinct()
.collect(Collectors.joining(" "));
if (GisStringUtils.isNotBlank(updatedFiles)) {
result.append(" ").append(updatedFiles);
}
return result.toString();
}

private static String statusFull(Repository repo, Status status, String localBranch) throws IOException {
var trackingBranch = new BranchConfig(repo.getConfig(), localBranch)
.getTrackingBranch()
.replace("refs/remotes/", "");
var result = new StringBuilder()
.append(GisStringUtils.NEWLINE)
.append(" ## ")
.append(coloringWord(localBranch, CL_GREEN))
.append("...")
.append(coloringWord(trackingBranch, CL_RED))
.append(" ")
.append(aheadBehind(repo));

var statusByFiles = new HashMap<String, List<FileStatus>>();

Function<FileStatus, Consumer<String>> computeStatus = s -> f -> {
var value = statusByFiles.computeIfAbsent(f, v -> new ArrayList<FileStatus>());
value.add(s);
statusByFiles.put(f, value);
};

// TODO how do we know of file rename status
status.getModified().stream().forEach(computeStatus.apply(MODIFIED));
status.getChanged().stream().forEach(computeStatus.apply(CHANGED));
status.getAdded().stream().forEach(computeStatus.apply(ADDED));
status.getMissing().stream().forEach(computeStatus.apply(MISSING));
status.getRemoved().stream().forEach(computeStatus.apply(REMOVED));
status.getUntracked().stream().forEach(computeStatus.apply(UNTRACKED));
status.getConflicting().stream().forEach(computeStatus.apply(CONFLICT));
status.getUntrackedFolders().stream().forEach(computeStatus.apply(UNTRACKED_DIRS));

statusByFiles.forEach((k, v) -> result.append(GisStringUtils.NEWLINE)
.append(" ")
.append(FileStatus.toPrint(k, v)));
return result.toString();
}

public static String status(Path f, boolean oneLiner) throws IOException, GitAPIException {
var repo = openRepo(f.resolve(".git"));
Status status;
try (var git = new Git(repo)) {
status = git.status()
.setIgnoreSubmodules(IgnoreSubmoduleMode.ALL)
.call();
}
if (status == null) {
return "";
}
var result = new StringBuilder(infof(getDirectoryName(repo.getWorkTree().toPath())));
var localBranch = repo.getBranch();
result.append(oneLiner
? statusOneLiner(repo, status, localBranch)
: statusFull(repo, status, localBranch));
return result.toString();
}
}
25 changes: 24 additions & 1 deletion src/main/java/org/nqm/command/Wrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.nqm.config.GisLog;
import org.nqm.GisException;
import org.nqm.config.GisLog;
import org.nqm.utils.StdOutUtils;

public final class Wrapper {
Expand Down Expand Up @@ -69,4 +70,26 @@ public static void forEachModuleWith(Predicate<Path> pred, String... args) throw
public static void forEachModuleDo(String... args) throws IOException {
forEachModuleWith(p -> true, args);
}

public static void forEachModuleStatus(Consumer<Path> gitStatus) throws IOException {
var gitModulesFilePath = getFileMarker();
var currentDir = currentDir();
try (var exe = Executors.newVirtualThreadPerTaskExecutor()) {
Optional.of(Path.of(currentDir))
.ifPresent(root -> exe.submit(() -> gitStatus.accept(root)));
Files.readAllLines(gitModulesFilePath.toPath()).stream()
.map(String::trim)
.filter(s -> s.startsWith("path"))
.map(s -> s.replace("path = ", ""))
.map(dir -> Path.of(currentDir, dir))
.filter(dir -> {
if (dir.toFile().exists()) {
return true;
}
StdOutUtils.errln("directory '%s' does not exist, will be ignored!".formatted("" + dir));
return false;
})
.forEach(dir -> exe.submit(() -> gitStatus.accept(dir)));
}
}
}
Loading

0 comments on commit c02ea08

Please sign in to comment.