Repository: JanDeDobbeleer/oh-my-posh
Pull request #6436 (2025-05-12 06:32:21)

Next
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [x] Tests for the changes have been added (for bug fixes / features).
- [x] Docs have been added/updated (for bug fixes / features).

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing/git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/config/segment_types.go b/src/config/segment_types.go
index 69c5249f..a7c4987a 100644
--- a/src/config/segment_types.go
+++ b/src/config/segment_types.go
@@ -110,6 +110,8 @@ const (
 	JAVA SegmentType = "java"
 	// API writes the output of a custom JSON API
 	HTTP SegmentType = "http"
+	// JUJUTSU writes Jujutsu source control information
+	JUJUTSU SegmentType = "jujutsu"
 	// JULIA writes which julia version is currently active
 	JULIA SegmentType = "julia"
 	// KOTLIN writes the active kotlin version
@@ -120,7 +122,7 @@ const (
 	LASTFM SegmentType = "lastfm"
 	// LUA writes the active lua version
 	LUA SegmentType = "lua"
-	// MERCURIAL writes the Mercurial source control information
+	// MERCURIAL writes Mercurial source control information
 	MERCURIAL SegmentType = "mercurial"
 	// MOJO writes the active version of Mojo and the name of the Magic virtual env
 	MOJO SegmentType = "mojo"
@@ -279,6 +281,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
 	IPIFY:           func() SegmentWriter { return &segments.IPify{} },
 	JAVA:            func() SegmentWriter { return &segments.Java{} },
 	HTTP:            func() SegmentWriter { return &segments.HTTP{} },
+	JUJUTSU:         func() SegmentWriter { return &segments.Jujutsu{} },
 	JULIA:           func() SegmentWriter { return &segments.Julia{} },
 	KOTLIN:          func() SegmentWriter { return &segments.Kotlin{} },
 	KUBECTL:         func() SegmentWriter { return &segments.Kubectl{} },
diff --git a/src/runtime/mock/environment.go b/src/runtime/mock/environment.go
index acca25c9..6a99399c 100644
--- a/src/runtime/mock/environment.go
+++ b/src/runtime/mock/environment.go
@@ -231,6 +231,11 @@ func (env *Environment) MockHgCommand(dir, returnValue string, args ...string) {
 	env.On("RunCommand", "hg", args).Return(returnValue, nil)
 }
 
+func (env *Environment) MockJjCommand(dir, returnValue string, args ...string) {
+	args = append([]string{"--repository", dir, "--ignore-working-copy", "--no-pager", "--color", "never"}, args...)
+	env.On("RunCommand", "jj", args).Return(returnValue, nil)
+}
+
 func (env *Environment) MockSvnCommand(dir, returnValue string, args ...string) {
 	args = append([]string{"-C", dir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
 	env.On("RunCommand", "svn", args).Return(returnValue, nil)
diff --git a/src/segments/jujutsu.go b/src/segments/jujutsu.go
new file mode 100644
index 00000000..34ad55fb
--- /dev/null
+++ b/src/segments/jujutsu.go
@@ -0,0 +1,118 @@
+package segments
+
+import (
+	"strings"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
+	"github.com/jandedobbeleer/oh-my-posh/src/runtime/path"
+)
+
+const (
+	JUJUTSUCOMMAND = "jj"
+
+	jjLogTemplate = `change_id.shortest() ++ "\n" ++ diff.summary()`
+)
+
+type JujutsuStatus struct {
+	ScmStatus
+}
+
+func (s *JujutsuStatus) add(code byte) {
+	switch code {
+	case 'D':
+		s.Deleted++
+	case 'A', 'C': // added, copied
+		s.Added++
+	case 'M':
+		s.Modified++
+	case 'R': // renamed
+		s.Moved++
+	}
+}
+
+type Jujutsu struct {
+	Working  *JujutsuStatus
+	ChangeID string
+	scm
+}
+
+func (jj *Jujutsu) Template() string {
+	return "jj {{.ChangeID}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }}"
+}
+
+func (jj *Jujutsu) Enabled() bool {
+	if !jj.shouldDisplay() {
+		return false
+	}
+
+	statusFormats := jj.props.GetKeyValueMap(StatusFormats, map[string]string{})
+	jj.Working = &JujutsuStatus{ScmStatus: ScmStatus{Formats: statusFormats}}
+
+	displayStatus := jj.props.GetBool(FetchStatus, false)
+	if displayStatus {
+		jj.setJujutsuStatus()
+	}
+
+	return true
+}
+
+func (jj *Jujutsu) CacheKey() (string, bool) {
+	dir, err := jj.env.HasParentFilePath(".jj", true)
+	if err != nil {
+		return "", false
+	}
+
+	return dir.Path, true
+}
+
+func (jj *Jujutsu) shouldDisplay() bool {
+	if !jj.hasCommand(JUJUTSUCOMMAND) {
+		return false
+	}
+
+	jjdir, err := jj.env.HasParentFilePath(".jj", false)
+	if err != nil {
+		return false
+	}
+
+	jj.setDir(jjdir.ParentFolder)
+
+	jj.mainSCMDir = jjdir.Path
+	jj.scmDir = jjdir.Path
+	// convert the worktree file path to a windows one when in a WSL shared folder
+	jj.repoRootDir = strings.TrimSuffix(jj.convertToWindowsPath(jjdir.Path), "/.jj")
+
+	return true
+}
+
+func (jj *Jujutsu) setDir(dir string) {
+	dir = path.ReplaceHomeDirPrefixWithTilde(dir) // align with template PWD
+	if jj.env.GOOS() == runtime.WINDOWS {
+		jj.Dir = strings.TrimSuffix(dir, `\.jj`)
+		return
+	}
+
+	jj.Dir = strings.TrimSuffix(dir, "/.jj")
+}
+
+func (jj *Jujutsu) setJujutsuStatus() {
+	// https://jj-vcs.github.io/jj/latest/templates/#commit-keywords
+	statusString, err := jj.getJujutsuCommandOutput("log", "-r", "@", "--no-graph", "-T", jjLogTemplate)
+	if err != nil {
+		return
+	}
+
+	lines := strings.Split(statusString, "\n")
+	jj.ChangeID = lines[0]
+
+	for _, line := range lines[1:] {
+		if len(line) > 0 {
+			jj.Working.add(line[0])
+		}
+	}
+}
+
+func (jj *Jujutsu) getJujutsuCommandOutput(command string, args ...string) (string, error) {
+	args = append([]string{"--repository", jj.repoRootDir, "--ignore-working-copy", "--no-pager", "--color", "never", command}, args...)
+	return jj.env.RunCommand(jj.command, args...)
+}
diff --git a/src/shell/scripts/omp.lua b/src/shell/scripts/omp.lua
index aaea80b8..488df30b 100644
--- a/src/shell/scripts/omp.lua
+++ b/src/shell/scripts/omp.lua
@@ -208,7 +208,9 @@ local function display_cached_prompt()
 end
 
 local function command_executed_mark(input)
-    no_exit_code = string.gsub(input, '^%s*(.-)%s*$', '%1') == ''
+    if string.gsub(input, '^%s*(.-)%s*$', '%1') ~= '' then
+        no_exit_code = false
+    end
     if ftcs_marks_enabled then
         clink.print('\x1b]133;C\007', NONL)
     end
diff --git a/themes/schema.json b/themes/schema.json
index 757a9b6e..fd98172d 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -342,6 +342,7 @@
             "http",
             "ipify",
             "java",
+            "jujutsu",
             "julia",
             "kotlin",
             "kubectl",
@@ -2398,6 +2399,37 @@
             }
           }
         },
+        {
+          "if": {
+            "properties": {
+              "type": {
+                "const": "jujutsu"
+              }
+            }
+          },
+          "then": {
+            "title": "Jujutsu Segment",
+            "description": "https://ohmyposh.dev/docs/segments/scm/jujutsu",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "fetch_status": {
+                    "type": "boolean",
+                    "title": "Display Status",
+                    "description": "Display the changes in the working copy",
+                    "default": false
+                  },
+                  "status_formats": {
+                    "$ref": "#/definitions/status_formats"
+                  },
+                  "native_fallback": {
+                    "$ref": "#/definitions/native_fallback"
+                  }
+                }
+              }
+            }
+          }
+        },
         {
           "if": {
             "properties": {
diff --git a/website/docs/segments/scm/jujutsu.mdx b/website/docs/segments/scm/jujutsu.mdx
new file mode 100644
index 00000000..7d9d0dea
--- /dev/null
+++ b/website/docs/segments/scm/jujutsu.mdx
@@ -0,0 +1,78 @@
+---
+id: jujutsu
+title: Jujutsu
+sidebar_label: Jujutsu
+---
+
+## What
+
+Display Jujutsu information when in a Jujutsu repository.
+
+## Sample Configuration
+
+import Config from "@site/src/components/Config.js";
+
+<Config
+  data={{
+    type: "jujutsu",
+    style: "powerline",
+    powerline_symbol: "\uE0B0",
+    foreground: "#193549",
+    background: "#ffeb3b",
+    properties: {
+      newprop: "\uEFF1",
+    },
+  }}
+/>
+
+## Properties
+
+### Fetching information
+
+As doing Jujutsu (jj) calls can slow down the prompt experience, we do not fetch information by default.
+Set `status_formats` to `true` to enable fetching additional information (and populate the template).
+
+| Name              |        Type         | Default | Description                                                                                                                                                                                                                                                      |
+| ----------------- | :-----------------: | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `fetch_status`    |      `boolean`      | `false` | fetch the local changes                                                                                                                                                                                                                                          |
+| `native_fallback` |      `boolean`      | `false` | when set to `true` and `jj.exe` is not available when inside a WSL2 shared Windows drive, we will fallback to the native `jj` executable to fetch data. Not all information can be displayed in this case                                                      |
+| `status_formats`  | `map[string]string` |         | a key, value map allowing to override how individual status items are displayed. For example, `"status_formats": { "Added": "Added: %d" }` will display the added count as `Added: 1` instead of `+1`. See the [Status](#status) section for available overrides |
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+jj {{.ChangeID}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }}
+```
+
+:::
+
+### Properties
+
+| Name                | Type       | Description                                           |
+| ------------------- | ---------- | ----------------------------------------------------- |
+| `.Working`          | `Status`   | changes in the working copy  (see below)              |
+| `.ChangeID`         | `string`   | The shortest unique prefix of the working copy change |
+
+### Status
+
+| Name         | Type      | Description                                  |
+| ------------ | --------- | -------------------------------------------- |
+| `.Modified`  | `int`     | number of modified files                     |
+| `.Deleted`   | `int`     | number of deleted files                      |
+| `.Added`     | `int`     | number of added files                        |
+| `.Moved`     | `int`     | number of renamed files                      |
+| `.Changed`   | `boolean` | if the status contains changes or not        |
+| `.String`    | `string`  | a string representation of the changes above |
+
+Local changes use the following syntax:
+
+| Icon | Description |
+| ---- | ----------- |
+| `~`  | Modified    |
+| `-`  | Deleted     |
+| `+`  | Added       |
+| `>`  | Moved       |
+
+[templates]: /docs/config-templates
diff --git a/website/docs/segments/scm/mercurial.mdx b/website/docs/segments/scm/mercurial.mdx
index 430044a6..3b407eee 100644
--- a/website/docs/segments/scm/mercurial.mdx
+++ b/website/docs/segments/scm/mercurial.mdx
@@ -31,7 +31,7 @@ import Config from "@site/src/components/Config.js";
 ### Fetching information
 
 As doing Mercurial (hg) calls can slow down the prompt experience, we do not fetch information by default.
-You can set the following properties to `true` to enable fetching additional information (and populate the template).
+You can set `fetch_status` to `true` to enable fetching additional information (and populate the template).
 
 | Name              |        Type         | Default | Description                                                                                                                                                                                                                                                      |
 | ----------------- | :-----------------: | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -56,7 +56,6 @@ hg {{.Branch}} {{if .LocalCommitNumber}}({{.LocalCommitNumber}}:{{.ChangeSetIDSh
 | `.Working`          | `Status`   | changes in the worktree (see below)                   |
 | `.IsTip`            | `boolean`  | Current commit is the tip commit                      |
 | `.ChangeSetID`      | `string`   | The current local commit number                       |
-| `.ChangeSetID`      | `string`   | The current local commit number                       |
 | `.ChangeSetIDShort` | `string`   | The current local commit number                       |
 | `.Branch`           | `string`   | current branch (releative URL reported by `svn info`) |
 | `.Bookmarks`        | `[]string` | the currently checked out revision number             |
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 7410a083..6678df2b 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -67,9 +67,9 @@ module.exports = {
           position: 'right',
         },
         {
-          href: 'https://www.gitkraken.com/invite/nQmDPR9D',
-          className: 'header-gk-link',
-          'aria-label': 'GitKraken',
+          href: 'https://www.warp.dev/oh-my-posh',
+          className: 'header-affiliate-link',
+          'aria-label': 'Warp',
           position: 'right',
         },
         {
@@ -131,8 +131,8 @@ module.exports = {
               href: 'https://polar.sh/oh-my-posh',
             },
             {
-              label: 'GitKraken',
-              href: 'https://www.gitkraken.com/invite/nQmDPR9D',
+              label: 'Warp',
+              href: 'https://www.warp.dev/oh-my-posh',
             },
             {
               label: 'Docusaurus',
diff --git a/website/sidebars.js b/website/sidebars.js
index 0c470baf..d1934873 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -169,6 +169,7 @@ module.exports = {
           items: [
             "segments/scm/fossil",
             "segments/scm/git",
+            "segments/scm/jujutsu",
             "segments/scm/mercurial",
             "segments/scm/plastic",
             "segments/scm/sapling",
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index 3f00a262..ad0e9274 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -71,21 +71,21 @@ html[data-theme="dark"] .header-github-link:before {
   padding-right: 10px;
 }
 
-.header-gk-link:hover {
+.header-affiliate-link:hover {
   opacity: 0.6;
 }
 
-.header-gk-link:before {
+.header-affiliate-link:before {
   content: "";
-  width: 26px;
-  height: 26px;
+  width: 24px;
+  height: 24px;
   display: flex;
-  background: url("data:image/svg+xml,%3Csvg viewBox='0 0 182 182' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m 176.70092,47.979262 a 3.6,3.6 0 0 0 -7,1.3 2.5,2.5 0 0 0 0.2,1.2 82.6,82.6 0 0 1 5.3,29.5 83.8,83.8 0 0 1 -72.7,82.999998 v -39.1 a 60.2,60.2 0 0 0 7.3,-1.9 v 32.8 a 77,77 0 0 0 19,-142.199998 3.7,3.7 0 0 0 -5.1,1.6 4.5,4.5 0 0 0 -0.3,1.6 3.6,3.6 0 0 0 1.8,3.2 69.7,69.7 0 0 1 -8.1,125.899998 v -29.1 a 11.2,11.2 0 0 0 7.8,-10.6 11,11 0 0 0 -5.5,-9.699998 c 2.6,-25.1 14.1,-18.5 14.1,-26.5 v -4.6 c 0,-12.1 -27.8,-51.1 -40.799999,-52.1 h -2.4 c -13,1 -40.8,40 -40.8,52.1 v 4.6 c 0,8 11.5,1.4 14.1,26.5 a 11.2,11.2 0 0 0 2.4,20.299998 v 29.1 a 69.7,69.7 0 0 1 -8.2,-125.899998 3.6,3.6 0 0 0 1.5,-4.8 3.5,3.5 0 0 0 -3.3,-2 3.3,3.3 0 0 0 -1.7,0.4 77,77 0 0 0 19,142.199998 v -32.8 a 60.2,60.2 0 0 0 7.3,1.9 v 39.1 A 83.8,83.8 0 0 1 7.8009215,79.979262 a 84.8,84.8 0 0 1 5.2999995,-29.5 3.6,3.6 0 0 0 -1.9,-4.6 4,4 0 0 0 -1.4999998,-0.3 3.7,3.7 0 0 0 -3.3999997,2.4 91,91 0 0 0 81.4999995,122.899998 v -46 h 7.4 v 46 a 90.9,90.9 0 0 0 87.299999,-90.899998 89.1,89.1 0 0 0 -5.8,-32 z m -68.4,51.9 a 7.5,7.5 0 1 1 10.6,10.599998 7.5,7.5 0 1 1 -10.6,-10.599998 z M 74.700921,110.47926 a 7.4,7.4 0 0 1 -10.5,0 7.5,7.5 0 0 1 0,-10.599998 7.4,7.4 0 0 1 10.5,0 7.5,7.5 0 0 1 0,10.599998 z'/%3E%3C/svg%3E")
+  background: url("data:image/svg+xml,%3Csvg fill='black' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg' class='css-1hsn8wk'%3E%3Cpath d='M51.0696 4.92194H88.7341C94.8309 4.92194 99.7736 10.0293 99.7736 16.3295V60.6175C99.7736 66.9179 94.8309 72.0252 88.7341 72.0252H34.835L51.0696 4.92194Z' fill='currentColor'%3E%3C/path%3E%3Cpath d='M41.2866 17.6346H10.9455C4.90046 17.6346 0 22.7419 0 29.0421V73.3302C0 79.6305 4.90046 84.7378 10.9455 84.7378H48.2888L49.7863 78.495H26.6878L41.2866 17.6346Z' %3E%3C/path%3E%3C/svg%3E")
     no-repeat;
 }
 
-html[data-theme="dark"] .header-gk-link:before {
-  background: url("data:image/svg+xml,%3Csvg viewBox='0 0 182 182' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='m 176.70092,47.979262 a 3.6,3.6 0 0 0 -7,1.3 2.5,2.5 0 0 0 0.2,1.2 82.6,82.6 0 0 1 5.3,29.5 83.8,83.8 0 0 1 -72.7,82.999998 v -39.1 a 60.2,60.2 0 0 0 7.3,-1.9 v 32.8 a 77,77 0 0 0 19,-142.199998 3.7,3.7 0 0 0 -5.1,1.6 4.5,4.5 0 0 0 -0.3,1.6 3.6,3.6 0 0 0 1.8,3.2 69.7,69.7 0 0 1 -8.1,125.899998 v -29.1 a 11.2,11.2 0 0 0 7.8,-10.6 11,11 0 0 0 -5.5,-9.699998 c 2.6,-25.1 14.1,-18.5 14.1,-26.5 v -4.6 c 0,-12.1 -27.8,-51.1 -40.799999,-52.1 h -2.4 c -13,1 -40.8,40 -40.8,52.1 v 4.6 c 0,8 11.5,1.4 14.1,26.5 a 11.2,11.2 0 0 0 2.4,20.299998 v 29.1 a 69.7,69.7 0 0 1 -8.2,-125.899998 3.6,3.6 0 0 0 1.5,-4.8 3.5,3.5 0 0 0 -3.3,-2 3.3,3.3 0 0 0 -1.7,0.4 77,77 0 0 0 19,142.199998 v -32.8 a 60.2,60.2 0 0 0 7.3,1.9 v 39.1 A 83.8,83.8 0 0 1 7.8009215,79.979262 a 84.8,84.8 0 0 1 5.2999995,-29.5 3.6,3.6 0 0 0 -1.9,-4.6 4,4 0 0 0 -1.4999998,-0.3 3.7,3.7 0 0 0 -3.3999997,2.4 91,91 0 0 0 81.4999995,122.899998 v -46 h 7.4 v 46 a 90.9,90.9 0 0 0 87.299999,-90.899998 89.1,89.1 0 0 0 -5.8,-32 z m -68.4,51.9 a 7.5,7.5 0 1 1 10.6,10.599998 7.5,7.5 0 1 1 -10.6,-10.599998 z M 74.700921,110.47926 a 7.4,7.4 0 0 1 -10.5,0 7.5,7.5 0 0 1 0,-10.599998 7.4,7.4 0 0 1 10.5,0 7.5,7.5 0 0 1 0,10.599998 z'/%3E%3C/svg%3E")
+html[data-theme="dark"] .header-affiliate-link:before {
+  background: url("data:image/svg+xml,%3Csvg fill='white' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg' class='css-1hsn8wk'%3E%3Cpath d='M51.0696 4.92194H88.7341C94.8309 4.92194 99.7736 10.0293 99.7736 16.3295V60.6175C99.7736 66.9179 94.8309 72.0252 88.7341 72.0252H34.835L51.0696 4.92194Z' %3E%3C/path%3E%3Cpath d='M41.2866 17.6346H10.9455C4.90046 17.6346 0 22.7419 0 29.0421V73.3302C0 79.6305 4.90046 84.7378 10.9455 84.7378H48.2888L49.7863 78.495H26.6878L41.2866 17.6346Z' %3E%3C/path%3E%3C/svg%3E")
     no-repeat;
 }
 


Test patch:
diff --git a/src/segments/jujutsu_test.go b/src/segments/jujutsu_test.go
new file mode 100644
index 00000000..35f4d67a
--- /dev/null
+++ b/src/segments/jujutsu_test.go
@@ -0,0 +1,117 @@
+package segments
+
+import (
+	"testing"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
+	"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestJujutsuEnabledToolNotFound(t *testing.T) {
+	env := new(mock.Environment)
+	env.On("InWSLSharedDrive").Return(false)
+	env.On("HasCommand", "jj").Return(false)
+	env.On("GOOS").Return("")
+	env.On("IsWsl").Return(false)
+
+	jj := &Jujutsu{}
+	jj.Init(properties.Map{}, env)
+
+	assert.False(t, jj.Enabled())
+}
+
+func TestJujutsuEnabledInWorkingDirectory(t *testing.T) {
+	fileInfo := &runtime.FileInfo{
+		Path:         "/dir/hello",
+		ParentFolder: "/dir",
+		IsDir:        true,
+	}
+	env := new(mock.Environment)
+	env.On("InWSLSharedDrive").Return(false)
+	env.On("HasCommand", "jj").Return(true)
+	env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil)
+	env.On("GOOS").Return("")
+
+	jj := &Jujutsu{}
+	jj.Init(properties.Map{}, env)
+
+	assert.True(t, jj.Enabled())
+	assert.Equal(t, fileInfo.Path, jj.mainSCMDir)
+	assert.Equal(t, fileInfo.Path, jj.repoRootDir)
+}
+
+func TestJujutsuGetIdInfo(t *testing.T) {
+	cases := []struct {
+		ExpectedWorking  *JujutsuStatus
+		Case             string
+		LogOutput        string
+		ExpectedChangeID string
+	}{
+		{
+			Case:             "nochanges",
+			LogOutput:        "a\n\n",
+			ExpectedChangeID: "a",
+			ExpectedWorking: &JujutsuStatus{ScmStatus{
+				Deleted:  0,
+				Added:    0,
+				Modified: 0,
+				Moved:    0,
+			}},
+		},
+		{
+			Case: "changed",
+			LogOutput: `b
+D deleted_file
+A added_file
+C {copied_file => new_file}
+M modified_file
+R {renamed_file => new_file}
+`,
+			ExpectedChangeID: "b",
+			ExpectedWorking: &JujutsuStatus{ScmStatus{
+				Deleted:  1,
+				Added:    2,
+				Modified: 1,
+				Moved:    1,
+			}},
+		},
+	}
+
+	for _, tc := range cases {
+		fileInfo := &runtime.FileInfo{
+			Path:         "/dir/hello",
+			ParentFolder: "/dir",
+			IsDir:        true,
+		}
+
+		props := properties.Map{
+			FetchStatus: true,
+		}
+
+		env := new(mock.Environment)
+		env.On("InWSLSharedDrive").Return(false)
+		env.On("HasCommand", "jj").Return(true)
+		env.On("GOOS").Return("")
+		env.On("IsWsl").Return(false)
+		env.On("HasParentFilePath", ".jj", false).Return(fileInfo, nil)
+		env.On("PathSeparator").Return("/")
+		env.On("Home").Return(poshHome)
+		env.On("Getenv", poshGitEnv).Return("")
+		env.MockJjCommand(fileInfo.Path, tc.LogOutput, "log", "-r", "@", "--no-graph", "-T", jjLogTemplate)
+
+		jj := &Jujutsu{}
+		jj.Init(props, env)
+
+		if tc.ExpectedWorking != nil {
+			tc.ExpectedWorking.Formats = map[string]string{}
+		}
+
+		assert.True(t, jj.Enabled())
+		assert.Equal(t, fileInfo.Path, jj.mainSCMDir)
+		assert.Equal(t, fileInfo.Path, jj.repoRootDir)
+		assert.Equal(t, tc.ExpectedWorking, jj.Working, tc.Case)
+		assert.Equal(t, tc.ExpectedChangeID, jj.ChangeID, tc.Case)
+	}
+}