Skip to content

Commit 14532ca

Browse files
committed
feat(jujutsu): expose commit ID prefix and rest
1 parent 2f6b628 commit 14532ca

4 files changed

Lines changed: 198 additions & 37 deletions

File tree

src/segments/jujutsu.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ const (
1616

1717
IgnoreWorkingCopy options.Option = "ignore_working_copy"
1818
ChangeIDMinLen options.Option = "change_id_min_len"
19+
CommitIDMinLen options.Option = "commit_id_min_len"
1920
FetchAhead options.Option = "fetch_ahead_counter"
2021
AheadIcon options.Option = "ahead_icon"
22+
23+
jujutsuStatusFrameHeader = "OMPJJ1:5"
24+
jujutsuStatusFieldCount = 5
2125
)
2226

2327
type JujutsuStatus struct {
@@ -42,6 +46,9 @@ type Jujutsu struct {
4246
ChangeID string
4347
ChangeIDPrefix string
4448
ChangeIDRest string
49+
CommitID string
50+
CommitIDPrefix string
51+
CommitIDRest string
4552
Scm
4653
}
4754

@@ -161,32 +168,46 @@ func (jj *Jujutsu) setJujutsuStatus() {
161168
return
162169
}
163170

164-
header, statusString, _ := strings.Cut(statusString, "\n")
165-
prefix, rest, found := strings.Cut(header, "|")
166-
// Jujutsu change IDs contain only canonical ID characters, so "|" is a safe
167-
// separator that survives RunCommand's trimming when rest is empty.
168-
if !found || len(prefix) == 0 || strings.Contains(rest, "|") {
171+
fields := strings.Split(statusString, "\x00")
172+
if len(fields) != jujutsuStatusFieldCount+2 ||
173+
fields[0] != jujutsuStatusFrameHeader ||
174+
fields[len(fields)-1] != "" ||
175+
fields[1] == "" ||
176+
fields[3] == "" {
169177
return
170178
}
171179

172-
jj.ChangeIDPrefix = prefix
173-
jj.ChangeIDRest = rest
174-
jj.ChangeID = prefix + rest
180+
changeIDPrefix := fields[1]
181+
changeIDRest := fields[2]
182+
commitIDPrefix := fields[3]
183+
commitIDRest := fields[4]
184+
statusString = fields[5]
175185

176186
for line := range strings.SplitSeq(statusString, "\n") {
177187
if len(line) > 0 {
178188
jj.Working.add(line[0])
179189
}
180190
}
191+
192+
jj.ChangeIDPrefix = changeIDPrefix
193+
jj.ChangeIDRest = changeIDRest
194+
jj.ChangeID = changeIDPrefix + changeIDRest
195+
jj.CommitIDPrefix = commitIDPrefix
196+
jj.CommitIDRest = commitIDRest
197+
jj.CommitID = commitIDPrefix + commitIDRest
181198
}
182199

183200
func (jj *Jujutsu) logTemplate() string {
184201
// https://jj-vcs.github.io/jj/latest/templates/#commit-keywords
185-
minLength := jj.options.Int(ChangeIDMinLen, 0)
202+
changeIDMinLength := jj.options.Int(ChangeIDMinLen, 0)
203+
commitIDMinLength := jj.options.Int(CommitIDMinLen, 0)
186204
return fmt.Sprintf(
187-
`change_id.shortest(%d).prefix() ++ "|" ++ change_id.shortest(%d).rest() ++ "\n" ++ diff.summary()`,
188-
minLength,
189-
minLength,
205+
`"%s\0" ++ change_id.shortest(%d).prefix() ++ "\0" ++ change_id.shortest(%d).rest() ++ "\0" ++ commit_id.shortest(%d).prefix() ++ "\0" ++ commit_id.shortest(%d).rest() ++ "\0" ++ diff.summary() ++ "\0"`,
206+
jujutsuStatusFrameHeader,
207+
changeIDMinLength,
208+
changeIDMinLength,
209+
commitIDMinLength,
210+
commitIDMinLength,
190211
)
191212
}
192213

src/segments/jujutsu_test.go

Lines changed: 153 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package segments
22

33
import (
44
"errors"
5+
"strings"
56
"testing"
67

78
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
@@ -10,7 +11,7 @@ import (
1011
"github.com/stretchr/testify/assert"
1112
)
1213

13-
func TestJujutsuEnabledToolNotFound(t *testing.T) {
14+
func TestJujutsuEnabledRepositoryNotFound(t *testing.T) {
1415
env := new(mock.Environment)
1516
env.On("InWSLSharedDrive").Return(false)
1617
env.On("HasParentFilePath", ".jj", false).Return(&runtime.FileInfo{}, errors.New("not found"))
@@ -23,15 +24,33 @@ func TestJujutsuEnabledToolNotFound(t *testing.T) {
2324
assert.False(t, jj.Enabled())
2425
}
2526

26-
func TestJujutsuEnabledInWorkingDirectory(t *testing.T) {
27+
func TestJujutsuEnabledToolNotFound(t *testing.T) {
2728
fileInfo := &runtime.FileInfo{
2829
Path: "/dir/hello",
2930
ParentFolder: "/dir",
3031
IsDir: true,
3132
}
3233
env := new(mock.Environment)
3334
env.On("InWSLSharedDrive").Return(false)
34-
env.On("HasCommand", "jj").Return(true)
35+
env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil)
36+
env.On("GOOS").Return("")
37+
env.On("HasCommand", "jj").Return(false)
38+
39+
jj := &Jujutsu{}
40+
jj.Init(options.Map{FetchStatus: true}, env)
41+
42+
assert.False(t, jj.Enabled())
43+
env.AssertNotCalled(t, "RunCommand")
44+
env.AssertExpectations(t)
45+
}
46+
47+
func TestJujutsuEnabledInWorkingDirectory(t *testing.T) {
48+
fileInfo := &runtime.FileInfo{
49+
Path: "/dir/hello",
50+
ParentFolder: "/dir",
51+
IsDir: true,
52+
}
53+
env := new(mock.Environment)
3554
env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil)
3655
env.On("GOOS").Return("")
3756

@@ -44,16 +63,35 @@ func TestJujutsuEnabledInWorkingDirectory(t *testing.T) {
4463
assert.Empty(t, jj.ChangeID)
4564
assert.Empty(t, jj.ChangeIDPrefix)
4665
assert.Empty(t, jj.ChangeIDRest)
66+
assert.Empty(t, jj.CommitID)
67+
assert.Empty(t, jj.CommitIDPrefix)
68+
assert.Empty(t, jj.CommitIDRest)
69+
env.AssertNotCalled(t, "HasCommand")
4770
env.AssertNotCalled(t, "RunCommand")
71+
env.AssertExpectations(t)
4872
}
4973

5074
func TestJujutsuLogTemplate(t *testing.T) {
5175
jj := &Jujutsu{}
52-
jj.Init(options.Map{ChangeIDMinLen: 8}, new(mock.Environment))
76+
jj.Init(options.Map{
77+
ChangeIDMinLen: 8,
78+
CommitIDMinLen: 12,
79+
}, new(mock.Environment))
5380

5481
assert.Equal(
5582
t,
56-
`change_id.shortest(8).prefix() ++ "|" ++ change_id.shortest(8).rest() ++ "\n" ++ diff.summary()`,
83+
`"OMPJJ1:5\0" ++ change_id.shortest(8).prefix() ++ "\0" ++ change_id.shortest(8).rest() ++ "\0" ++ commit_id.shortest(12).prefix() ++ "\0" ++ commit_id.shortest(12).rest() ++ "\0" ++ diff.summary() ++ "\0"`,
84+
jj.logTemplate(),
85+
)
86+
}
87+
88+
func TestJujutsuLogTemplateDefaults(t *testing.T) {
89+
jj := &Jujutsu{}
90+
jj.Init(options.Map{}, new(mock.Environment))
91+
92+
assert.Equal(
93+
t,
94+
`"OMPJJ1:5\0" ++ change_id.shortest(0).prefix() ++ "\0" ++ change_id.shortest(0).rest() ++ "\0" ++ commit_id.shortest(0).prefix() ++ "\0" ++ commit_id.shortest(0).rest() ++ "\0" ++ diff.summary() ++ "\0"`,
5795
jj.logTemplate(),
5896
)
5997
}
@@ -62,6 +100,15 @@ func TestJujutsuTemplate(t *testing.T) {
62100
assert.Equal(t, " \uf1fa{{.ChangeID}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }} ", (&Jujutsu{}).Template())
63101
}
64102

103+
func jujutsuStatusFrame(changeIDPrefix, changeIDRest, commitIDPrefix, commitIDRest, status string) string {
104+
return jujutsuStatusFrameHeader + "\x00" +
105+
changeIDPrefix + "\x00" +
106+
changeIDRest + "\x00" +
107+
commitIDPrefix + "\x00" +
108+
commitIDRest + "\x00" +
109+
status + "\x00"
110+
}
111+
65112
func TestJujutsuGetIdInfo(t *testing.T) {
66113
cases := []struct {
67114
ExpectedWorking *JujutsuStatus
@@ -71,17 +118,25 @@ func TestJujutsuGetIdInfo(t *testing.T) {
71118
ExpectedChangeID string
72119
ExpectedChangeIDPrefix string
73120
ExpectedChangeIDRest string
121+
ExpectedCommitID string
122+
ExpectedCommitIDPrefix string
123+
ExpectedCommitIDRest string
74124
ChangeIDMinLen int
75-
ValidHeader bool
125+
CommitIDMinLen int
126+
ValidFrame bool
76127
}{
77128
{
78-
Case: "clean with minimum-length rest",
79-
LogOutput: "t|ususrrr",
129+
Case: "clean with minimum-length rests",
130+
LogOutput: jujutsuStatusFrame("t", "ususrrr", "a", "bcdefghijk", ""),
80131
ExpectedChangeID: "tususrrr",
81132
ExpectedChangeIDPrefix: "t",
82133
ExpectedChangeIDRest: "ususrrr",
134+
ExpectedCommitID: "abcdefghijk",
135+
ExpectedCommitIDPrefix: "a",
136+
ExpectedCommitIDRest: "bcdefghijk",
83137
ChangeIDMinLen: 8,
84-
ValidHeader: true,
138+
CommitIDMinLen: 11,
139+
ValidFrame: true,
85140
ExpectedWorking: &JujutsuStatus{ScmStatus{
86141
Deleted: 0,
87142
Added: 0,
@@ -90,13 +145,17 @@ func TestJujutsuGetIdInfo(t *testing.T) {
90145
}},
91146
},
92147
{
93-
Case: "clean with empty rest after output trimming",
94-
LogOutput: "tususrrr|",
148+
Case: "clean with empty rests after output trimming",
149+
LogOutput: strings.TrimSpace(jujutsuStatusFrame("tususrrr", "", "abcdef12", "", "") + "\n"),
95150
ExpectedChangeID: "tususrrr",
96151
ExpectedChangeIDPrefix: "tususrrr",
97152
ExpectedChangeIDRest: "",
153+
ExpectedCommitID: "abcdef12",
154+
ExpectedCommitIDPrefix: "abcdef12",
155+
ExpectedCommitIDRest: "",
98156
ChangeIDMinLen: 4,
99-
ValidHeader: true,
157+
CommitIDMinLen: 4,
158+
ValidFrame: true,
100159
ExpectedWorking: &JujutsuStatus{ScmStatus{
101160
Deleted: 0,
102161
Added: 0,
@@ -105,18 +164,21 @@ func TestJujutsuGetIdInfo(t *testing.T) {
105164
}},
106165
},
107166
{
108-
Case: "changed with empty rest",
109-
LogOutput: `tususrrr|
110-
D deleted_file
167+
Case: "changed with empty rests",
168+
LogOutput: jujutsuStatusFrame("tususrrr", "", "abcdef12", "", `D deleted_file
111169
A added_file
112170
C {copied_file => new_file}
113171
M modified_file
114-
R {renamed_file => new_file}`,
172+
R {renamed_file => new_file}`),
115173
ExpectedChangeID: "tususrrr",
116174
ExpectedChangeIDPrefix: "tususrrr",
117175
ExpectedChangeIDRest: "",
176+
ExpectedCommitID: "abcdef12",
177+
ExpectedCommitIDPrefix: "abcdef12",
178+
ExpectedCommitIDRest: "",
118179
ChangeIDMinLen: 4,
119-
ValidHeader: true,
180+
CommitIDMinLen: 4,
181+
ValidFrame: true,
120182
ExpectedWorking: &JujutsuStatus{ScmStatus{
121183
Deleted: 1,
122184
Added: 2,
@@ -126,9 +188,70 @@ R {renamed_file => new_file}`,
126188
},
127189
{
128190
Case: "command error",
129-
LogOutput: "ignored",
191+
LogOutput: jujutsuStatusFrame("t", "ususrrr", "a", "bcdefghijk", ""),
130192
CommandError: errors.New("jj failed"),
131193
ChangeIDMinLen: 8,
194+
CommitIDMinLen: 11,
195+
ExpectedWorking: &JujutsuStatus{ScmStatus{
196+
Deleted: 0,
197+
Added: 0,
198+
Modified: 0,
199+
Moved: 0,
200+
}},
201+
},
202+
{
203+
Case: "wrong magic",
204+
LogOutput: "OMPJJ2:5\x00t\x00ususrrr\x00a\x00bcdefghijk\x00D deleted_file\x00",
205+
ChangeIDMinLen: 8,
206+
CommitIDMinLen: 11,
207+
ExpectedWorking: &JujutsuStatus{ScmStatus{
208+
Deleted: 0,
209+
Added: 0,
210+
Modified: 0,
211+
Moved: 0,
212+
}},
213+
},
214+
{
215+
Case: "wrong field count in magic",
216+
LogOutput: "OMPJJ1:4\x00t\x00ususrrr\x00a\x00bcdefghijk\x00D deleted_file\x00",
217+
ChangeIDMinLen: 8,
218+
CommitIDMinLen: 11,
219+
ExpectedWorking: &JujutsuStatus{ScmStatus{
220+
Deleted: 0,
221+
Added: 0,
222+
Modified: 0,
223+
Moved: 0,
224+
}},
225+
},
226+
{
227+
Case: "missing terminal sentinel",
228+
LogOutput: "OMPJJ1:5\x00t\x00ususrrr\x00a\x00bcdefghijk\x00D deleted_file",
229+
ChangeIDMinLen: 8,
230+
CommitIDMinLen: 11,
231+
ExpectedWorking: &JujutsuStatus{ScmStatus{
232+
Deleted: 0,
233+
Added: 0,
234+
Modified: 0,
235+
Moved: 0,
236+
}},
237+
},
238+
{
239+
Case: "missing field",
240+
LogOutput: "OMPJJ1:5\x00t\x00ususrrr\x00a\x00D deleted_file\x00",
241+
ChangeIDMinLen: 8,
242+
CommitIDMinLen: 11,
243+
ExpectedWorking: &JujutsuStatus{ScmStatus{
244+
Deleted: 0,
245+
Added: 0,
246+
Modified: 0,
247+
Moved: 0,
248+
}},
249+
},
250+
{
251+
Case: "extra field",
252+
LogOutput: "OMPJJ1:5\x00t\x00ususrrr\x00a\x00bcdefghijk\x00unexpected\x00D deleted_file\x00",
253+
ChangeIDMinLen: 8,
254+
CommitIDMinLen: 11,
132255
ExpectedWorking: &JujutsuStatus{ScmStatus{
133256
Deleted: 0,
134257
Added: 0,
@@ -137,9 +260,10 @@ R {renamed_file => new_file}`,
137260
}},
138261
},
139262
{
140-
Case: "missing delimiter",
141-
LogOutput: "tususrrr\nD deleted_file",
263+
Case: "empty change ID prefix",
264+
LogOutput: jujutsuStatusFrame("", "tususrrr", "a", "bcdefghijk", "D deleted_file"),
142265
ChangeIDMinLen: 8,
266+
CommitIDMinLen: 11,
143267
ExpectedWorking: &JujutsuStatus{ScmStatus{
144268
Deleted: 0,
145269
Added: 0,
@@ -148,9 +272,10 @@ R {renamed_file => new_file}`,
148272
}},
149273
},
150274
{
151-
Case: "multiple delimiters",
152-
LogOutput: "t|usus|rrr\nD deleted_file",
275+
Case: "empty commit ID prefix",
276+
LogOutput: jujutsuStatusFrame("t", "ususrrr", "", "abcdefghijk", "D deleted_file"),
153277
ChangeIDMinLen: 8,
278+
CommitIDMinLen: 11,
154279
ExpectedWorking: &JujutsuStatus{ScmStatus{
155280
Deleted: 0,
156281
Added: 0,
@@ -171,6 +296,7 @@ R {renamed_file => new_file}`,
171296
props := options.Map{
172297
FetchStatus: true,
173298
ChangeIDMinLen: tc.ChangeIDMinLen,
299+
CommitIDMinLen: tc.CommitIDMinLen,
174300
}
175301

176302
env := new(mock.Environment)
@@ -207,8 +333,12 @@ R {renamed_file => new_file}`,
207333
assert.Equal(t, tc.ExpectedChangeID, jj.ChangeID)
208334
assert.Equal(t, tc.ExpectedChangeIDPrefix, jj.ChangeIDPrefix)
209335
assert.Equal(t, tc.ExpectedChangeIDRest, jj.ChangeIDRest)
210-
if tc.ValidHeader {
336+
assert.Equal(t, tc.ExpectedCommitID, jj.CommitID)
337+
assert.Equal(t, tc.ExpectedCommitIDPrefix, jj.CommitIDPrefix)
338+
assert.Equal(t, tc.ExpectedCommitIDRest, jj.CommitIDRest)
339+
if tc.ValidFrame {
211340
assert.Equal(t, jj.ChangeIDPrefix+jj.ChangeIDRest, jj.ChangeID)
341+
assert.Equal(t, jj.CommitIDPrefix+jj.CommitIDRest, jj.CommitID)
212342
}
213343
env.AssertNumberOfCalls(t, "RunCommand", 1)
214344
env.AssertExpectations(t)

themes/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,6 +2534,12 @@
25342534
"description": "Minimum length of the change ID to display",
25352535
"default": 0
25362536
},
2537+
"commit_id_min_len": {
2538+
"type": "integer",
2539+
"title": "Commit ID Minimum Length",
2540+
"description": "Minimum length of the commit ID to display",
2541+
"default": 0
2542+
},
25372543
"fetch_status": {
25382544
"type": "boolean",
25392545
"title": "Display Status",

0 commit comments

Comments
 (0)