feat: add mercurial segment

This commit is contained in:
David Courtney 2023-01-09 19:47:05 -05:00 committed by Jan De Dobbeleer
parent 41f2716237
commit 822b7c755e
7 changed files with 462 additions and 0 deletions

View file

@ -161,6 +161,8 @@ const (
KUBECTL SegmentType = "kubectl"
// LUA writes the active lua version
LUA SegmentType = "lua"
// MERCURIAL writes the Mercurial source control information
MERCURIAL SegmentType = "mercurial"
// NBGV writes the nbgv version information
NBGV SegmentType = "nbgv"
// NIGHTSCOUT is an open source diabetes system
@ -267,6 +269,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
KOTLIN: func() SegmentWriter { return &segments.Kotlin{} },
KUBECTL: func() SegmentWriter { return &segments.Kubectl{} },
LUA: func() SegmentWriter { return &segments.Lua{} },
MERCURIAL: func() SegmentWriter { return &segments.Mercurial{} },
NBGV: func() SegmentWriter { return &segments.Nbgv{} },
NIGHTSCOUT: func() SegmentWriter { return &segments.Nightscout{} },
NODE: func() SegmentWriter { return &segments.Node{} },

View file

@ -228,6 +228,11 @@ func (env *MockedEnvironment) MockGitCommand(dir, returnValue string, args ...st
env.On("RunCommand", "git", args).Return(returnValue, nil)
}
func (env *MockedEnvironment) MockHgCommand(dir, returnValue string, args ...string) {
args = append([]string{"-R", dir}, args...)
env.On("RunCommand", "hg", args).Return(returnValue, nil)
}
func (env *MockedEnvironment) 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)

168
src/segments/mercurial.go Normal file
View file

@ -0,0 +1,168 @@
package segments
import (
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
)
const (
MERCURIALCOMMAND = "hg"
hgLogTemplate = "{rev}|{node}|{branch}|{tags}|{bookmarks}"
)
type MercurialStatus struct {
ScmStatus
}
func (s *MercurialStatus) add(code string) {
switch code {
case "R", "!":
s.Deleted++
case "A":
s.Added++
case "?":
s.Untracked++
case "M":
s.Modified++
}
}
type Mercurial struct {
scm
Working *MercurialStatus
IsTip bool
LocalCommitNumber string
ChangeSetID string
ChangeSetIDShort string
Branch string
Bookmarks []string
Tags []string
}
func (hg *Mercurial) Template() string {
return "hg {{.Branch}} {{if .LocalCommitNumber}}({{.LocalCommitNumber}}:{{.ChangeSetIDShort}}){{end}}{{range .Bookmarks }} \uf02e {{.}}{{end}}{{range .Tags}} \uf02b {{.}}{{end}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }}" //nolint: lll
}
func (hg *Mercurial) Enabled() bool {
if !hg.shouldDisplay() {
return false
}
hg.Working = &MercurialStatus{}
displayStatus := hg.props.GetBool(FetchStatus, false)
if displayStatus {
hg.setMercurialStatus()
}
return true
}
func (hg *Mercurial) shouldDisplay() bool {
if !hg.hasCommand(MERCURIALCOMMAND) {
return false
}
hgdir, err := hg.env.HasParentFilePath(".hg")
if err != nil {
return false
}
if hg.shouldIgnoreRootRepository(hgdir.ParentFolder) {
return false
}
hg.setDir(hgdir.ParentFolder)
hg.workingDir = hgdir.Path
hg.rootDir = hgdir.Path
// convert the worktree file path to a windows one when in a WSL shared folder
hg.realDir = strings.TrimSuffix(hg.convertToWindowsPath(hgdir.Path), "/.hg")
return true
}
func (hg *Mercurial) setDir(dir string) {
dir = platform.ReplaceHomeDirPrefixWithTilde(hg.env, dir) // align with template PWD
if hg.env.GOOS() == platform.WINDOWS {
hg.Dir = strings.TrimSuffix(dir, `\.hg`)
return
}
hg.Dir = strings.TrimSuffix(dir, "/.hg")
}
func (hg *Mercurial) setMercurialStatus() {
hg.Branch = hg.command
idString := hg.getHgCommandOutput("log", "-r", ".", "--template", hgLogTemplate)
if len(idString) == 0 {
return
}
idSplit := strings.Split(idString, "|")
if len(idSplit) != 5 {
return
}
hg.LocalCommitNumber = idSplit[0]
hg.ChangeSetID = idSplit[1]
if len(hg.ChangeSetID) >= 12 {
hg.ChangeSetIDShort = hg.ChangeSetID[:12]
}
hg.Branch = idSplit[2]
hg.Tags = doSplit(idSplit[3])
hg.Bookmarks = doSplit(idSplit[4])
hg.IsTip = false
tipIndex := 0
for i, tag := range hg.Tags {
if tag == "tip" {
hg.IsTip = true
tipIndex = i
break
}
}
if hg.IsTip {
hg.Tags = RemoveAtIndex(hg.Tags, tipIndex)
}
statusString := hg.getHgCommandOutput("status")
if len(statusString) == 0 {
return
}
statusLines := strings.Split(statusString, "\n")
for _, status := range statusLines {
hg.Working.add(status[:1])
}
}
func doSplit(s string) []string {
if len(s) == 0 {
return []string{}
}
return strings.Split(s, " ")
}
func RemoveAtIndex(s []string, index int) []string {
ret := make([]string, 0)
ret = append(ret, s[:index]...)
return append(ret, s[index+1:]...)
}
func (hg *Mercurial) getHgCommandOutput(command string, args ...string) string {
args = append([]string{"-R", hg.realDir, command}, args...)
val, err := hg.env.RunCommand(hg.command, args...)
if err != nil {
return ""
}
return strings.TrimSpace(val)
}

View file

@ -0,0 +1,180 @@
package segments
import (
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/mock"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/stretchr/testify/assert"
)
func TestMercurialEnabledToolNotFound(t *testing.T) {
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "hg").Return(false)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
hg := &Mercurial{
scm: scm{
env: env,
props: properties.Map{},
},
}
assert.False(t, hg.Enabled())
}
func TestMercurialEnabledInWorkingDirectory(t *testing.T) {
fileInfo := &platform.FileInfo{
Path: "/dir/hello",
ParentFolder: "/dir",
IsDir: true,
}
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "hg").Return(true)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
env.On("HasParentFilePath", ".hg").Return(fileInfo, nil)
env.On("PathSeparator").Return("/")
env.On("Home").Return("/Users/posh")
env.On("Getenv", poshGitEnv).Return("")
hg := &Mercurial{
scm: scm{
env: env,
props: properties.Map{},
},
}
assert.True(t, hg.Enabled())
assert.Equal(t, fileInfo.Path, hg.workingDir)
assert.Equal(t, fileInfo.Path, hg.realDir)
}
func TestMercurialGetIdInfo(t *testing.T) {
cases := []struct {
Case string
LogOutput string
StatusOutput string
ExpectedWorking *MercurialStatus
ExpectedBranch string
ExpectedChangeSetID string
ExpectedShortID string
ExpectedLocalCommitNumber string
ExpectedIsTip bool
ExpectedBookmarks []string
ExpectedTags []string
}{
{
Case: "nochanges_tip",
LogOutput: "123|b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500|branchname|tip|",
StatusOutput: "",
ExpectedWorking: &MercurialStatus{ScmStatus{
Modified: 0,
Added: 0,
Deleted: 0,
Moved: 0,
Untracked: 0,
Conflicted: 0,
}},
ExpectedBranch: "branchname",
ExpectedChangeSetID: "b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500",
ExpectedShortID: "b6cb23dcb79f",
ExpectedLocalCommitNumber: "123",
ExpectedIsTip: true,
ExpectedBookmarks: []string{},
ExpectedTags: []string{},
},
{
Case: "nochanges",
LogOutput: "123|b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500|branchname||",
StatusOutput: "",
ExpectedWorking: &MercurialStatus{ScmStatus{
Modified: 0,
Added: 0,
Deleted: 0,
Moved: 0,
Untracked: 0,
Conflicted: 0,
}},
ExpectedBranch: "branchname",
ExpectedChangeSetID: "b6cb23dcb79fe5c2215f1ae8f1a85326a7fed500",
ExpectedShortID: "b6cb23dcb79f",
ExpectedLocalCommitNumber: "123",
ExpectedIsTip: false,
ExpectedBookmarks: []string{},
ExpectedTags: []string{},
},
{
Case: "changed",
LogOutput: "3|11a953bf0288663b530dd6d65f3c8e0d5f7fddb5|default|tip mytag mytag2|bm1 bm2",
StatusOutput: `
M Modified.File
? Untracked.File
R Removed.File
! AlsoRemoved.File
A Added.File
`,
ExpectedWorking: &MercurialStatus{ScmStatus{
Modified: 1,
Added: 1,
Deleted: 2,
Moved: 0,
Untracked: 1,
Conflicted: 0,
}},
ExpectedBranch: "default",
ExpectedChangeSetID: "11a953bf0288663b530dd6d65f3c8e0d5f7fddb5",
ExpectedShortID: "11a953bf0288",
ExpectedLocalCommitNumber: "3",
ExpectedIsTip: true,
ExpectedBookmarks: []string{"bm1", "bm2"},
ExpectedTags: []string{"mytag", "mytag2"},
},
}
for _, tc := range cases {
fileInfo := &platform.FileInfo{
Path: "/dir/hello",
ParentFolder: "/dir",
IsDir: true,
}
props := properties.Map{
FetchStatus: true,
}
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "hg").Return(true)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
env.On("HasParentFilePath", ".hg").Return(fileInfo, nil)
env.On("PathSeparator").Return("/")
env.On("Home").Return("/Users/posh")
env.On("Getenv", poshGitEnv).Return("")
env.MockHgCommand(fileInfo.Path, tc.LogOutput, "log", "-r", ".", "--template", hgLogTemplate)
env.MockHgCommand(fileInfo.Path, tc.StatusOutput, "status")
hg := &Mercurial{
scm: scm{
env: env,
props: props,
},
}
assert.True(t, hg.Enabled())
assert.Equal(t, fileInfo.Path, hg.workingDir)
assert.Equal(t, fileInfo.Path, hg.realDir)
assert.Equal(t, tc.ExpectedWorking, hg.Working, tc.Case)
assert.Equal(t, tc.ExpectedBranch, hg.Branch, tc.Case)
assert.Equal(t, tc.ExpectedChangeSetID, hg.ChangeSetID, tc.Case)
assert.Equal(t, tc.ExpectedShortID, hg.ChangeSetIDShort, tc.Case)
assert.Equal(t, tc.ExpectedLocalCommitNumber, hg.LocalCommitNumber, tc.Case)
assert.Equal(t, tc.ExpectedIsTip, hg.IsTip, tc.Case)
assert.Equal(t, tc.ExpectedBookmarks, hg.Bookmarks, tc.Case)
assert.Equal(t, tc.ExpectedTags, hg.Tags, tc.Case)
}
}

View file

@ -249,6 +249,7 @@
"kotlin",
"kubectl",
"lua",
"mercurial",
"node",
"npm",
"nx",
@ -2947,6 +2948,29 @@
"title": "Display GitVersion segment",
"description": "https://ohmyposh.dev/docs/segments/gitversion"
}
},
{
"if": {
"properties": {
"type": { "const": "mercurial" }
}
},
"then": {
"title": "Mercurial Segment",
"description": "https://ohmyposh.dev/docs/mercurial",
"properties": {
"properties": {
"properties": {
"fetch_status": {
"type": "boolean",
"title": "Display Status",
"description": "Display the local changes or not",
"default": true
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,81 @@
---
id: mercurial
title: Mercurial
sidebar_label: Mercurial
---
## What
Display Mercurial information when in a Mercurial repository. For maximum compatibility,
make sure your `hg` executable is up-to-date (when branch or status information is incorrect for example).
## Sample Configuration
```json
{
"type": "mercurial",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#193549",
"background": "#ffeb3b",
"properties": {
"newprop": "\uEFF1"
}
}
```
## Properties
### 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).
| Name | Type | Description |
| ----------------- | --------- | --------------------------------------------- |
| `fetch_status` | `boolean` | fetch the local changes - defaults to `false` |
## Template ([info][templates])
:::note default template
```template
hg {{.Branch}} {{if .LocalCommitNumber}}({{.LocalCommitNumber}}:{{.ChangeSetIDShort}}){{end}}{{range .Bookmarks }} \uf02e {{.}}{{end}}{{range .Tags}} \uf02b {{.}}{{end}}{{if .Working.Changed}} \uf044 {{ .Working.String }}{{ end }}
```
:::
### Properties
| Name | Type | Description |
| -------------------- | ----------------- | ----------------------------------------------------- |
| `.Working` | `MercurialStatus` | 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 |
| `.Tags` | `[]string` | the currently checked out revision number |
### SvnStatus
| Name | Type | Description |
| --------------- | --------- | ---------------------------------------------- |
| `.Untracked` | `int` | number of files not under version control |
| `.Modified` | `int` | number of modified files |
| `.Deleted` | `int` | number of deleted files |
| `.Added` | `int` | number of added 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 |
| ---- | ----------- |
| `?` | untracked |
| `+` | added |
| `-` | deleted |
| `~` | modified |
[templates]: /docs/config-templates

View file

@ -84,6 +84,7 @@ module.exports = {
"segments/kotlin",
"segments/kubectl",
"segments/lua",
"segments/mercurial",
"segments/nbgv",
"segments/nightscout",
"segments/node",