Skip to content

Commit 40c8696

Browse files
author
minh
committed
feat: support zsh autocomplete
fix: #82
1 parent 46d4389 commit 40c8696

File tree

7 files changed

+167
-7
lines changed

7 files changed

+167
-7
lines changed

Containerfile

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ USER root
1313
WORKDIR /app/gis
1414
COPY --from=build /app/gis/target/gis-*.jar target/
1515
COPY --from=build /app/gis/target/lib target/lib
16-
RUN native-image -march=compatibility -cp target/gis-*.jar "org.nqm.Gis" --no-fallback -H:IncludeResources=".properties"
16+
RUN native-image -march=compatibility -cp target/gis-*.jar "org.nqm.Gis" --no-fallback \
17+
-H:IncludeResources=".properties" \
18+
-H:IncludeResources="_gis"
19+
1720
RUN mv org.nqm.gis gis
1821
RUN chmod +x gis
1922
RUN ./gis --version

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ After the steps above, an executable file named `gis` will be created under proj
2727

2828
```shell script
2929
cd gis
30-
mvn clean package
30+
mvn clean verify package
3131
```
3232
The executable jar file will be created at `target/gis-<version>.jar`
3333

@@ -38,6 +38,14 @@ For more details, just run:
3838
./gis --help
3939
```
4040

41+
Generate completion for zsh:
42+
```
43+
./gis completion --directory ${fpath[1]}
44+
```
45+
Reload your zsh session, we can now press `<TAB>` for autocomplete.
46+
47+
Currently gis only support zsh for completion.
48+
4149
# Config
4250

4351
Gis will read config from file at `~/.config/gis.config`

src/main/java/org/nqm/Gis.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.nqm.config.GisLog;
66
import org.nqm.exception.GisException;
77
import picocli.CommandLine;
8+
import picocli.AutoComplete.GenerateCompletion;
89
import picocli.CommandLine.Command;
910
import picocli.CommandLine.IExecutionExceptionHandler;
1011
import picocli.CommandLine.Option;
@@ -13,6 +14,7 @@
1314

1415
@Command(
1516
name = "gis",
17+
subcommands = GenerateCompletion.class,
1618
description = "Git extension wrapper which supports submodules",
1719
mixinStandardHelpOptions = true,
1820
versionProvider = GisVersion.class)
@@ -34,7 +36,12 @@ public static void setVerbose(boolean verbose) {
3436

3537
public static void main(String... args) {
3638
var gis = new CommandLine(new Gis());
37-
gis.setExecutionExceptionHandler(GLOBAL_EXCEPTION_HANLER);
39+
gis.setExecutionExceptionHandler(GLOBAL_EXCEPTION_HANLER)
40+
.getSubcommands()
41+
.get("generate-completion")
42+
.getCommandSpec()
43+
.usageMessage()
44+
.hidden(true);
3845

3946
gis.execute(args.length == 0
4047
? new String[] {GIT_STATUS, "--one-line"}

src/main/java/org/nqm/command/GitCommand.java

+35-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.nqm.command.Wrapper.forEachModuleWith;
55
import static org.nqm.config.GisConfig.currentDir;
66
import java.io.BufferedReader;
7+
import java.io.FileOutputStream;
78
import java.io.IOException;
89
import java.io.InputStreamReader;
910
import java.nio.file.Files;
@@ -33,6 +34,8 @@ public class GitCommand {
3334
private static final String ORIGIN = "origin";
3435
private static final Path TMP_FILE = Path.of("/", "tmp", "gis_fetch" + currentDir().replace("/", "_"));
3536

37+
static final String GIS_AUTOCOMPLETE_FILE = "_gis";
38+
3639
public static final String GIT_STATUS = "status";
3740
public static final String HOOKS_OPTION = "--hooks";
3841

@@ -93,7 +96,8 @@ void checkoutNewBranch(
9396
@Parameters(index = "0", paramLabel = "<new_branch_name>",
9497
description = "branch name") String newBranch,
9598
@Parameters(paramLabel = "<modules>",
96-
description = "Specified modules. If empty, will create for all submodules and root.") String... modules) throws IOException {
99+
description = "Specified modules. If empty, will create for all submodules and root.") String... modules)
100+
throws IOException {
97101
if (null == modules || modules.length < 1) {
98102
forEachModuleDo(CHECKOUT, "-b", newBranch);
99103
return;
@@ -148,7 +152,8 @@ void remotePruneOrigin() throws IOException {
148152
}
149153

150154
@Command(name = "local-prune", aliases = "prune")
151-
void localPrune(@Parameters(index = "0", paramLabel = "<default branch name>") String branch) throws IOException {
155+
void localPrune(@Parameters(index = "0", paramLabel = "<default branch name>") String branch)
156+
throws IOException {
152157
forEachModuleDo("for-each-ref",
153158
"--merged=%s".formatted(branch),
154159
"--format=%(refname:short)",
@@ -160,14 +165,16 @@ void localPrune(@Parameters(index = "0", paramLabel = "<default branch name>") S
160165
}
161166

162167
@Command(name = "stash")
163-
void stash(@Option(names = "-pp", description = "pop first stashed changes") boolean isPop) throws IOException {
168+
void stash(@Option(names = "-pp", description = "pop first stashed changes") boolean isPop)
169+
throws IOException {
164170
var args = isPop ? new String[] {"stash", "pop"} : new String[] {"stash"};
165171
forEachModuleDo(args);
166172
}
167173

168174
@Command(name = "branches")
169175
void listBranches(
170-
@Option(names = "-nn", description = "do not print module name") boolean noPrintModuleName) throws IOException {
176+
@Option(names = "-nn", description = "do not print module name") boolean noPrintModuleName)
177+
throws IOException {
171178
var sArgs = Stream.of("for-each-ref", "--format=%(refname:short)", "refs/heads/");
172179
if (noPrintModuleName) {
173180
sArgs = Stream.concat(sArgs, Stream.of("--gis-no-print-modules-name"));
@@ -193,6 +200,30 @@ void files() throws IOException {
193200
forEachModuleDo("diff", "--name-only", "--gis-concat-modules-name");
194201
}
195202

203+
@Command(name = "completion", description = "generate an zsh auto completion script")
204+
void generateCompletion(
205+
@Option(names = "--directory",
206+
description = "export completion zsh function to file at specified directory") Path dir)
207+
throws IOException {
208+
try (var stream = this.getClass().getClassLoader().getResourceAsStream(GIS_AUTOCOMPLETE_FILE)) {
209+
var buffer = new BufferedReader(new InputStreamReader(stream));
210+
String line = null;
211+
if (dir != null) {
212+
var file = dir.resolve(GIS_AUTOCOMPLETE_FILE);
213+
try (var out = new FileOutputStream(file.toFile())) {
214+
while ((line = buffer.readLine()) != null) {
215+
out.write(line.getBytes());
216+
out.write("%n".formatted().getBytes());
217+
}
218+
}
219+
return;
220+
}
221+
while ((line = buffer.readLine()) != null) {
222+
StdOutUtils.println(line);
223+
}
224+
}
225+
}
226+
196227
private static Stream<String> streamOf(String[] input) {
197228
return Stream.of(input).map(String::trim).distinct();
198229
}

src/main/resources/_gis

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#compdef gis
2+
compdef _gis gis
3+
4+
function _gis {
5+
local line
6+
_arguments -C\
7+
"1: :(checkout co \
8+
checkout-branch cb \
9+
fetch fe \
10+
fetch-origin fo \
11+
files \
12+
init \
13+
branches \
14+
local-prune prune \
15+
pull pu \
16+
push pus \
17+
rebase-current-origin ru \
18+
rebase-origin re \
19+
remote-prune-origin rpo \
20+
remove-branch rm \
21+
stash \
22+
status st)" \
23+
"--help[print help]" \
24+
"--version[print gis version]" \
25+
"*::arg:->args"
26+
case $line[1] in
27+
checkout | co | fetch-origin | fo | local-prune | prune | push | pus | rebase-origin | re | remove-branch | rm)
28+
_suggest_branches
29+
;;
30+
branches)
31+
_gis_branches_suggest
32+
;;
33+
stash)
34+
_gis_stash_suggest
35+
;;
36+
status | st)
37+
_gis_status_suggest
38+
;;
39+
esac
40+
}
41+
42+
function get_branches {
43+
[ -d .git ] && git for-each-ref --format="%(refname:short)" refs/heads refs/remotes
44+
}
45+
46+
function _suggest_branches {
47+
_arguments "1: :($(get_branches))"
48+
}
49+
50+
function _gis_branches_suggest {
51+
_arguments "-nn[do not print module name]"
52+
}
53+
54+
function _gis_status_suggest {
55+
_arguments "--one-line[print result in one line]"
56+
}
57+
58+
function _gis_stash_suggest {
59+
_arguments "-pp[pop first stashed changes]"
60+
}

src/test/java/org/nqm/command/GitCommandTest.java

+48
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.mockito.ArgumentMatchers.any;
66
import static org.mockito.Mockito.times;
77
import static org.mockito.Mockito.verify;
8+
import static org.nqm.command.GitCommand.GIS_AUTOCOMPLETE_FILE;
89
import static org.nqm.config.GisConfig.GIT_HOME_DIR;
910
import java.io.ByteArrayInputStream;
1011
import java.io.IOException;
@@ -380,4 +381,51 @@ void pushOrigin_OK() throws IOException {
380381
System.setIn(in);
381382
}
382383
}
384+
385+
@Test
386+
void generateCompletionToConsole_OK() throws IOException {
387+
// when:
388+
gis.generateCompletion(null);
389+
390+
// then:
391+
assertThat(outCaptor).hasToString("""
392+
this is a completion
393+
script for test gis
394+
in zsh.
395+
""");
396+
}
397+
398+
@Test
399+
void generateCompletionToFile_OK() throws IOException {
400+
// when:
401+
gis.generateCompletion(tempPath);
402+
403+
// then:
404+
var content = Files.readString(tempPath.resolve(GIS_AUTOCOMPLETE_FILE));
405+
assertThat(content).isEqualTo("""
406+
this is a completion
407+
script for test gis
408+
in zsh.
409+
""");
410+
}
411+
412+
@Test
413+
void generateCompletionToFile_withFileAlreadyExist_shouldOverwrite() throws IOException {
414+
// given:
415+
var file = tempPath.resolve(GIS_AUTOCOMPLETE_FILE);
416+
Files.createFile(file);
417+
Files.writeString(file, "this is some existing text");
418+
419+
// when:
420+
gis.generateCompletion(tempPath);
421+
422+
// then:
423+
var content = Files.readString(file);
424+
assertThat(content).isEqualTo("""
425+
this is a completion
426+
script for test gis
427+
in zsh.
428+
""");
429+
}
430+
383431
}

src/test/resources/_gis

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
this is a completion
2+
script for test gis
3+
in zsh.

0 commit comments

Comments
 (0)