Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Nightly

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write

jobs:
nightly:
name: Nightly Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for git describe

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true

- name: Get version info
id: version
run: |
BASE=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
SHORT=$(git rev-parse --short HEAD)
echo "version=${BASE}-nightly.${SHORT}" >> $GITHUB_OUTPUT

- name: Build Binaries
run: |
mkdir -p dist
platforms=("linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
for platform in "${platforms[@]}"; do
os_arch=(${platform//\// })
GOOS=${os_arch[0]}
GOARCH=${os_arch[1]}

GOOS=$GOOS GOARCH=$GOARCH go build \
-ldflags="-s -w -X github.com/versenilvis/iris/root.Version=${{ steps.version.outputs.version }}" \
-trimpath -o "dist/iris" main.go

cd dist
tar -czf "iris_${GOOS}_${GOARCH}.tar.gz" "iris"
rm "iris"
cd ..
done

- name: Delete existing nightly release
run: |
gh release delete nightly --yes 2>/dev/null || true
git tag -d nightly 2>/dev/null || true
git push origin --delete nightly 2>/dev/null || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create Nightly Release
uses: softprops/action-gh-release@v1
with:
tag_name: nightly
name: ${{ steps.version.outputs.version }}
body: |
🌙 **Nightly build** - `${{ steps.version.outputs.version }}`

auto-built from commit `${{ github.sha }}`

> install manually, this build will **not** trigger an update notification
files: dist/*.tar.gz
prerelease: true
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion commands/core/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func Register(s *Spec) {
Registry[s.Name] = s
}

// ResetRegistry clears all registered specs use in tests only
// ResetRegistry clears all registered specs - use in tests only
func ResetRegistry() {
Registry = make(map[string]*Spec)
}
140 changes: 99 additions & 41 deletions commands/dev/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,104 @@ import (
"context"
"os/exec"
"strings"
"time"

"github.com/versenilvis/iris/commands/core"
)

// GitRemoteGenerator suggests git remotes
func GitRemoteGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
return getGitResults(prefix, "remote")
func GitRemoteGenerator(tokens []string, _ string, _ string) []core.Suggestion {
return getGitResults(tokens, "remote")
}

// GitStashGenerator suggests git stashes
func GitStashGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
return getGitResults(prefix, "stash", "list", "--format=%gd: %gs")
func GitStashGenerator(tokens []string, _ string, _ string) []core.Suggestion {
return getGitResults(tokens, "stash", "list", "--format=%gd: %gs")
}

func getGitResults(prefix string, args ...string) []core.Suggestion {
func getGitResults(tokens []string, args ...string) []core.Suggestion {
return getGitResultsFiltered(tokens, false, args...)
}

func getGitResultsFiltered(tokens []string, localOnly bool, args ...string) []core.Suggestion {
cwd := core.GetCWD()
cmd := exec.CommandContext(context.Background(), "git", args...)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = cwd
out, err := cmd.Output()
if err != nil {
return nil
}

activeBranch := ""
switch args[0] {
case "branch":
// Try to find the current active branch to filter it out later
activeCmd := exec.CommandContext(context.Background(), "git", "rev-parse", "--abbrev-ref", "HEAD")
if args[0] == "branch" {
activeCmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD")
activeCmd.Dir = cwd
if activeOut, err := activeCmd.Output(); err == nil {
activeBranch = strings.TrimSpace(string(activeOut))
}
}

seen := make(map[string]bool)
lines := strings.Split(string(out), "\n")
var results []core.Suggestion

// more robust subcommand detection: find the first non-flag after "git"
subcommand := ""
for i := 1; i < len(tokens); i++ {
if !strings.HasPrefix(tokens[i], "-") {
subcommand = tokens[i]
break
}
}

for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "*") { // skip active branch marker if any
line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
if line == "" {
continue
}

isRemote := strings.HasPrefix(line, "remotes/")

// skip remote tracking branches if localOnly mode
if localOnly && isRemote {
continue
}

// strip remotes/ prefix to get the usable form: origin/main
if isRemote {
line = strings.TrimPrefix(line, "remotes/")
}

// only skip active branch for checkout/switch commands
if subcommand == "checkout" || subcommand == "switch" {
if line == activeBranch {
continue
}
// also skip any remote branch that tracks the active branch (e.g. origin/main)
if idx := strings.Index(line, "/"); isRemote && idx != -1 {
if line[idx+1:] == activeBranch {
continue
}
}
}

// dedup: origin/dev and dev would both appear with -a; skip if already seen the short name
shortName := line
if idx := strings.Index(line, "/"); isRemote && idx != -1 {
shortName = line[idx+1:] // "origin/dev" -> "dev"
}
if line == "" || line == activeBranch {
if seen[shortName] {
continue
}

// handle remote branches that look like "remotes/origin/main"
line = strings.TrimPrefix(line, "remotes/")
seen[shortName] = true

suggestionCmd := line
suggestionDesc := args[0]

// for stash list, the format is "stash@{0}: message"
if args[0] == "stash" {
parts := strings.SplitN(line, ": ", 2)
if len(parts) == 2 {
Expand All @@ -65,15 +111,16 @@ func getGitResults(prefix string, args ...string) []core.Suggestion {
}

results = append(results, core.Suggestion{
Cmd: prefix + " " + suggestionCmd,
Cmd: suggestionCmd,
Desc: suggestionDesc,
})
}
return results
}

// GitBranchGenerator suggests git branches
func GitBranchGenerator(tokens []string, prefix string, partial string) []core.Suggestion {

// GitBranchGenerator suggests git branches (local + remote, deduped)
func GitBranchGenerator(tokens []string, _ string, _ string) []core.Suggestion {
// check if we are in "create" mode (-b or -B or -c)
isCreateMode := false
for _, t := range tokens {
Expand All @@ -87,35 +134,46 @@ func GitBranchGenerator(tokens []string, prefix string, partial string) []core.S
return nil
}

return getGitResults(prefix, "branch", "-a", "--format=%(refname:short)")
return getGitResults(tokens, "branch", "-a", "--format=%(refname:short)")
}

// gitLocalBranchGenerator is like GitBranchGenerator but only local branches
// used for push/pull where remote tracking branches cause duplicates
func gitLocalBranchGenerator(tokens []string, _ string, _ string) []core.Suggestion {
isCreateMode := false
for _, t := range tokens {
if t == "-b" || t == "-B" || t == "-c" || t == "-C" {
isCreateMode = true
break
}
}
if isCreateMode {
return nil
}
return getGitResultsFiltered(tokens, true, "branch", "-a", "--format=%(refname:short)")
}

// GitPushPullGenerator suggests remotes for the first arg, and branches for the second

func GitPushPullGenerator(tokens []string, prefix string, partial string) []core.Suggestion {
// Filter out flags to find positional arguments
args := []string{}
for i := 1; i < len(tokens); i++ {
// count completed positional args (not flags, not the partial being typed)
// tokens[0] = "git", tokens[1] = "push"/"pull", so start at 2
// exclude tokens[len-1] because that's the partial being typed
pArgs := []string{}
for i := 2; i < len(tokens)-1; i++ {
t := tokens[i]
if t != "" && !strings.HasPrefix(t, "-") {
args = append(args, t)
if t == "" || strings.HasPrefix(t, "-") {
continue
}
pArgs = append(pArgs, t)
}

// args[0] is subcommand (push/pull)
// args[1] should be remote
// args[2] should be branch

// If we only have subcommand, suggest remotes
if len(args) == 1 {
// no remote confirmed yet, suggest remotes
if len(pArgs) == 0 {
return GitRemoteGenerator(tokens, prefix, partial)
}

// If we have subcommand + remote, suggest branches
if len(args) == 2 {
return GitBranchGenerator(tokens, prefix, partial)
}

return nil

// remote is set, suggest local branches only (no duplicates with origin/xxx)
return gitLocalBranchGenerator(tokens, prefix, partial)
}

func init() {
Expand Down Expand Up @@ -279,7 +337,7 @@ func init() {
{
Name: "tag",
Description: "manage tags",
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { return getGitResults(prefix, "tag", "-l") },
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion { return getGitResults(tokens, "tag", "-l") },
Options: []core.Option{
{Name: "-a", Description: "annotated tag"},
{Name: "-d", Description: "delete tag"},
Expand Down
5 changes: 5 additions & 0 deletions root/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ func IsNewer(current, latest string) bool {
return false
}

// nightly builds are never shown as stable update targets
if strings.Contains(l, "-nightly.") {
return false
}

if c == l {
return false
}
Expand Down
Loading
Loading