Repository: JanDeDobbeleer/oh-my-posh (Go)

Issue:
'Git' segment generates invalid URL for Azure DevOps repositories
### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

### What happened?

I have the following segment in my config, so I can click on an icon and have the repo open in the browser:

```json
{
  "type": "git",
  "style": "plain",
  "template": "{{ url .UpstreamIcon .UpstreamURL }}",
  "properties": {
    "fetch_upstream_icon": true
  }
}
```

It works perfectly fine with GitHub 👌

However, for repositories hosted on Azure DevOps, it generates an invalid URL.

If cloned via **HTTPS**:

```
$ git remote -v
origin  https://<organization>@dev.azure.com/<organization>/<project>/_git/<repository> (fetch)
origin  https://<organization>@dev.azure.com/<organization>/<project>/_git/<repository> (push)
```

The segment generates this URL ❌:

```
https://<organization>@dev.azure.com/<organization>/<project>/_git/<repository>
```

If cloned via **SSH**:

```
$ git remote -v
origin  git@ssh.dev.azure.com:v3/<organization>/<project>/<repository> (fetch)
origin  git@ssh.dev.azure.com:v3/<organization>/<project>/<repository> (push)
```

The segment generates this URL ❌:

```
https://ssh.dev.azure.com/v3/<organization>/<project>/<repository>
```

However, none of the above URLs work. A valid URL should look like this ✅:

```
https://dev.azure.com/<organization>/<project>/_git/<repository>
```

### Theme

Default theme, but then customized (removed a few segments and blocks, but didn't add/edit to have caused a bug)

### What OS are you seeing the problem on?

Linux

### Which shell are you using?

bash

### Log output

```Shell
Version: 20.2.0

Shell: bash (5.1.16(1)-release)

Prompt:

  ~  

Segments:

ConsoleTitle(true)                        -   1 ms
Text(true)                                -   0 ms
Path(true)                                -   2 ms
Git(false)                                -   1 ms
Status(true)                              -   2 ms

Run duration: 15.799076ms

Cache path: /home/<username>/.cache/oh-my-posh

Config path: /full/path/to/my/config.json

Logs:

[DEBUG] 12:40:33.669 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.669 shell.go:Getenv(TERM_PROGRAM) - 183.871µs
[DEBUG] 12:40:33.669 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.669 shell.go:Getenv(OMP_CACHE_DIR) - 12.12µs
[DEBUG] 12:40:33.669 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.669 shell.go:Getenv(XDG_CACHE_HOME) - 9.101µs
[TRACE] 12:40:33.669 shell_unix.go:CachePath() - 80.809µs
[DEBUG] 12:40:33.670 shell.go:Getenv:326 → /full/path/to/my/config.json
[TRACE] 12:40:33.670 shell.go:Getenv(POSH_THEME) - 27.368µs
[DEBUG] 12:40:33.670 shell_unix.go:Platform:87 → ubuntu
[TRACE] 12:40:33.670 shell.go:resolveConfigPath() - 74.734µs
[TRACE] 12:40:33.670 shell.go:Init() - 931.28µs
[TRACE] 12:40:33.670 shell.go:Flags() - 198ns
[TRACE] 12:40:33.673 config.go:loadConfig() - 2.639572ms
[TRACE] 12:40:33.673 shell.go:Flags() - 251ns
[DEBUG] 12:40:33.673 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.673 shell.go:Getenv(OMP_CACHE_DISABLED) - 23.924µs
[DEBUG] 12:40:33.673 shell.go:Shell:601 → no shell name provided in flags, trying to detect it
[DEBUG] 12:40:33.676 shell.go:Shell:609 → process name: bash
[TRACE] 12:40:33.676 shell.go:Shell() - 3.391591ms
[DEBUG] 12:40:33.676 shell.go:Getenv:326 → 5.1.16(1)-release
[TRACE] 12:40:33.676 shell.go:Getenv(POSH_SHELL_VERSION) - 59.949µs
[DEBUG] 12:40:33.676 debug.go:PrintDebug:22 → Segment: Title
[DEBUG] 12:40:33.676 text.go:Render:71 → Rendering template: {{ .PWD }}
[TRACE] 12:40:33.678 shell_unix.go:Root() - 3.347µs
[TRACE] 12:40:33.678 shell.go:Shell() - 239ns
[TRACE] 12:40:33.678 shell.go:StatusCodes() - 239ns
[DEBUG] 12:40:33.678 shell_unix.go:IsWsl:35 → false
[TRACE] 12:40:33.678 shell_unix.go:IsWsl() - 32.795µs
[DEBUG] 12:40:33.678 shell.go:TemplateCache:782 → environment: [SHELL=/bin/bash SESSION_MANAGER=local/<hostname>:@/tmp/.ICE-unix/44952,unix/<hostname>:/tmp/.ICE-unix/44952 QT_ACCESSIBILITY=1 COLORTERM=truecolor XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg SSH_AGENT_LAUNCHER=gnome-keyring XDG_MENU_PREFIX=gnome- GNOME_DESKTOP_SESSION_ID=this-is-deprecated GTK_IM_MODULE=ibus POSH_PID=66220 LANGUAGE=en_AU:en MANDATORY_PATH=/usr/share/gconf/ubuntu.mandatory.path GNOME_SHELL_SESSION_MODE=ubuntu SSH_AUTH_SOCK=/run/user/1000/keyring/ssh XMODIFIERS=@im=ibus DESKTOP_SESSION=ubuntu GTK_MODULES=gail:atk-bridge PWD=/home/<username> LOGNAME=<username> XDG_SESSION_DESKTOP=ubuntu XDG_SESSION_TYPE=x11 GPG_AGENT_INFO=/run/user/1000/gnupg/S.gpg-agent:0:1 SYSTEMD_EXEC_PID=61013 XAUTHORITY=/run/user/1000/gdm/Xauthority WINDOWPATH=3 HOME=/home/<username> USERNAME=<username> LANG=en_AU.UTF-8 LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36: XDG_CURRENT_DESKTOP=ubuntu:GNOME POSH_SHELL_VERSION=5.1.16(1)-release VTE_VERSION=6800 CONDA_PROMPT_MODIFIER=false GNOME_TERMINAL_SCREEN=/org/gnome/Terminal/screen/4d4949fc_3e73_4a39_85b7_e049b18ade50 LESSCLOSE=/usr/bin/lesspipe %s %s XDG_SESSION_CLASS=user TERM=xterm-256color DEFAULTS_PATH=/usr/share/gconf/ubuntu.default.path LESSOPEN=| /usr/bin/lesspipe %s USER=<username> GNOME_TERMINAL_SERVICE=:1.140 DISPLAY=:2 SHLVL=1 QT_IM_MODULE=ibus POSH_THEME=/full/path/to/my/config.json XDG_RUNTIME_DIR=/run/user/1000 XDG_DATA_DIRS=/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop PATH=/home/<username>/.node/node-v20.16.0-linux-x64/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin:/home/<username>/.dotnet/tools GDMSESSION=ubuntu DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus POWERLINE_COMMAND=oh-my-posh _=/usr/local/bin/oh-my-posh]
[TRACE] 12:40:33.678 shell.go:GOOS() - 311ns
[DEBUG] 12:40:33.678 shell.go:Pwd:356 → /home/<username>
[TRACE] 12:40:33.678 shell.go:Pwd() - 77.692µs
[TRACE] 12:40:33.678 shell.go:GOOS() - 182ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 120ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 119ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 131ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 184ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 122ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 120ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 117ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 119ns
[DEBUG] 12:40:33.678 shell.go:User:494 → <username>
[TRACE] 12:40:33.678 shell.go:User() - 11.655µs
[DEBUG] 12:40:33.678 shell.go:Host:511 → <hostname>
[TRACE] 12:40:33.678 shell.go:Host() - 16.151µs
[TRACE] 12:40:33.678 shell.go:GOOS() - 190ns
[DEBUG] 12:40:33.678 shell_unix.go:Platform:87 → ubuntu
[DEBUG] 12:40:33.678 shell.go:Getenv:326 → 1
[TRACE] 12:40:33.678 shell.go:Getenv(SHLVL) - 10.212µs
[TRACE] 12:40:33.678 shell.go:TemplateCache() - 549.108µs
[DEBUG] 12:40:33.678 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.678 shell.go:Getenv(POSH_CURSOR_LINE) - 9.988µs
[DEBUG] 12:40:33.678 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.678 shell.go:Getenv(POSH_CURSOR_COLUMN) - 8.781µs
[TRACE] 12:40:33.678 shell.go:Flags() - 242ns
[TRACE] 12:40:33.678 shell.go:Flags() - 118ns
[TRACE] 12:40:33.678 shell.go:Shell() - 286ns
[TRACE] 12:40:33.678 shell.go:Shell() - 142ns
[TRACE] 12:40:33.678 shell.go:Flags() - 251ns
[DEBUG] 12:40:33.678 properties.go:GetString:28 → {{ .Code }}
[TRACE] 12:40:33.678 shell.go:Pwd() - 601ns
[TRACE] 12:40:33.678 shell.go:GOOS() - 208ns
[DEBUG] 12:40:33.678 segment.go:SetEnabled:533 → Segment: Status
[TRACE] 12:40:33.678 shell.go:StatusCodes() - 275ns
[DEBUG] 12:40:33.678 text.go:Render:71 → Rendering template: {{ .Code }}
[TRACE] 12:40:33.679 shell.go:Flags() - 993ns
[TRACE] 12:40:33.679 shell.go:Pwd() - 911ns
[TRACE] 12:40:33.679 shell.go:GOOS() - 211ns
[TRACE] 12:40:33.679 shell.go:Flags() - 696ns
[DEBUG] 12:40:33.679 segment.go:SetEnabled:533 → Segment: Path
[TRACE] 12:40:33.679 shell.go:Pwd() - 311ns
[TRACE] 12:40:33.679 shell.go:Flags() - 777ns
[TRACE] 12:40:33.679 shell.go:Shell() - 442ns
[TRACE] 12:40:33.679 shell.go:Shell() - 115ns
[TRACE] 12:40:33.679 shell.go:Pwd() - 833ns
[TRACE] 12:40:33.679 shell.go:GOOS() - 207ns
[DEBUG] 12:40:33.679 segment.go:SetEnabled:533 → Segment: Git
[TRACE] 12:40:33.679 shell.go:Pwd() - 740ns
[TRACE] 12:40:33.679 shell.go:GOOS() - 223ns
[DEBUG] 12:40:33.679 shell_unix.go:IsWsl:35 → false
[DEBUG] 12:40:33.679 segment.go:SetEnabled:533 → Segment: Text
[TRACE] 12:40:33.679 shell_unix.go:IsWsl() - 15.88µs
[DEBUG] 12:40:33.679 properties.go:GetBool:22 → mapped_locations_enabled: true
[TRACE] 12:40:33.679 shell_unix.go:IsWsl2() - 27.993µs
[DEBUG] 12:40:33.679 properties.go:GetString:28 → 
[TRACE] 12:40:33.679 shell.go:GOOS() - 218ns
[TRACE] 12:40:33.679 shell.go:TemplateCache() - 1.307µs
[DEBUG] 12:40:33.679 properties.go:GetString:28 → 
[TRACE] 12:40:33.679 shell.go:GOOS() - 208ns
[DEBUG] 12:40:33.679 properties.go:GetString:28 → ~
[DEBUG] 12:40:33.679 properties.go:GetKeyValueMap:46 → mapped_locations: map[]
[TRACE] 12:40:33.679 shell.go:PathSeparator() - 221ns
[TRACE] 12:40:33.679 shell.go:GOOS() - 179ns
[TRACE] 12:40:33.679 shell.go:GOOS() - 124ns
[DEBUG] 12:40:33.679 shell.go:CommandPath:553 → /usr/bin/git
[TRACE] 12:40:33.679 shell.go:CommandPath(git) - 169.213µs
[TRACE] 12:40:33.679 shell.go:HasCommand(git) - 184.152µs
[TRACE] 12:40:33.679 shell.go:Pwd() - 317ns
[ERROR] 12:40:33.680 shell.go:HasParentFilePath:697 → stat .git: no such file or directory
[TRACE] 12:40:33.680 shell.go:TemplateCache() - 815ns
[TRACE] 12:40:33.680 shell.go:HasParentFilePath(.git) - 146.7µs
[DEBUG] 12:40:33.681 properties.go:GetBool:22 → fetch_bare_info: false
[DEBUG] 12:40:33.681 properties.go:GetBool:22 → always_enabled: true
[TRACE] 12:40:33.681 shell.go:GOOS() - 473ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 157ns
[TRACE] 12:40:33.681 shell.go:TemplateCache() - 1.726µs
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 225ns
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 127ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 150ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 202ns
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 117ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 164ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 174ns
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 221ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 158ns
[TRACE] 12:40:33.681 shell.go:GOOS() - 394ns
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 269ns
[DEBUG] 12:40:33.681 properties.go:GetString:28 → NO DATA
[TRACE] 12:40:33.681 shell.go:PathSeparator() - 235ns
[TRACE] 12:40:33.681 shell.go:Pwd() - 331ns
[DEBUG] 12:40:33.681 shell_unix.go:IsWsl:35 → false
[TRACE] 12:40:33.681 shell_unix.go:IsWsl() - 11.466µs
[TRACE] 12:40:33.681 shell.go:StackCount() - 206ns
[TRACE] 12:40:33.682 shell_unix.go:DirIsWritable(/home/<username>) - 12.858µs
[TRACE] 12:40:33.682 shell.go:TemplateCache() - 454ns
[DEBUG] 12:40:33.682 text.go:Render:71 → Rendering template: 
[DEBUG] 12:40:33.682 text.go:Render:71 → Rendering template:  {{ path .Path .Location }} 
[TRACE] 12:40:33.682 shell.go:Shell() - 478ns
[DEBUG] 12:40:33.682 text.go:Render:71 → Rendering template:  
[TRACE] 12:40:33.682 shell.go:Shell() - 240ns
[TRACE] 12:40:33.684 shell.go:TemplateCache() - 813ns
[TRACE] 12:40:33.684 shell.go:Shell() - 264ns
[DEBUG] 12:40:33.684 text.go:Render:71 → Rendering template: plain
[DEBUG] 12:40:33.684 text.go:Render:71 → Rendering template: plain
[DEBUG] 12:40:33.684 text.go:Render:71 → Rendering template: plain
[DEBUG] 12:40:33.684 text.go:Render:71 → Rendering template: {{ if gt .Code 0 }}p:red{{ end }}
[TRACE] 12:40:33.685 shell.go:TemplateCache() - 468ns
[DEBUG] 12:40:33.685 text.go:Render:71 → Rendering template: plain
[TRACE] 12:40:33.685 shell.go:Shell() - 313ns
[TRACE] 12:40:33.685 shell.go:Shell() - 122ns
[TRACE] 12:40:33.685 shell.go:Flags() - 173ns
[TRACE] 12:40:33.685 shell.go:Shell() - 79ns
[TRACE] 12:40:33.685 shell.go:Shell() - 89ns
[DEBUG] 12:40:33.685 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.685 shell.go:Getenv(OMP_CACHE_DIR) - 9.152µs
[DEBUG] 12:40:33.685 shell.go:Getenv:326 → NO DATA
[TRACE] 12:40:33.685 shell.go:Getenv(XDG_CACHE_HOME) - 22.503µs
[TRACE] 12:40:33.685 shell_unix.go:CachePath() - 68.189µs
[TRACE] 12:40:33.685 shell.go:Flags() - 95ns
```



Discussion:
The `.UpstreamURL` depends on the URL used when the repo was cloned. Since there are no common rules or conventions for URLs in different cloning methods used by different Git-based code hosting sites, I think it can be tricky (or perhaps impossible) to ensure a clean URL for an arbitrary site. Though, it's feasible to just add a set of cases for Azure URLs.
Oh, nice one 👌 Thanks @JanDeDobbeleer for the quick turnaround 🙏

Although... the PR only caters for HTTPS; if that's OK, I've opened a follow-up PR to support SSH as well:

#5581 
This issue has been automatically locked since there has not been any recent activity (i.e. last half year) after it was closed. It helps our maintainers focus on the active issues.
If you have found a problem that seems similar, please open a [discussion](https://github.com/JanDeDobbeleer/oh-my-posh/discussions/new?category=troubleshoot) first, complete the body with all the details necessary to reproduce, and mention this issue as reference.



Patch:
diff --git a/src/segments/git.go b/src/segments/git.go
index 51770e8712ff..30d8fc3056a9 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -451,6 +451,14 @@ func (g *Git) setBranchStatus() {
 }
 
 func (g *Git) cleanUpstreamURL(url string) string {
+	// https://{organization}@dev.azure.com/{organization}/{project}/_git/{repository}
+	if strings.Contains(url, "@dev.azure.com") {
+		match := regex.FindNamedRegexMatch(`.*@(?P<URL>dev.azure.com.*)`, url)
+		if len(match) != 0 {
+			return fmt.Sprintf("https://%s", match["URL"])
+		}
+	}
+
 	if strings.HasPrefix(url, "http") {
 		return url
 	}


Test patch:
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index dfdf66e63644..3f56adad6b68 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -649,11 +649,12 @@ func TestGitCleanSSHURL(t *testing.T) {
 		{Case: "gitea no port, no user", Expected: "https://src.example.com/user/repo", Upstream: "_gitea@src.example.com:user/repo.git"},
 		{Case: "git@ with user", Expected: "https://github.com/JanDeDobbeleer/oh-my-posh", Upstream: "git@github.com:JanDeDobbeleer/oh-my-posh"},
 		{Case: "unsupported", Upstream: "\\test\\repo.git"},
+		{Case: "Azure DevOps", Expected: "https://dev.azure.com/posh/oh-my-posh/_git/website", Upstream: "https://posh@dev.azure.com/posh/oh-my-posh/_git/website"},
 	}
 	for _, tc := range cases {
 		g := &Git{}
-		upstreamIcon := g.cleanUpstreamURL(tc.Upstream)
-		assert.Equal(t, tc.Expected, upstreamIcon, tc.Case)
+		upstreamURL := g.cleanUpstreamURL(tc.Upstream)
+		assert.Equal(t, tc.Expected, upstreamURL, tc.Case)
 	}
 }