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
7 changes: 7 additions & 0 deletions .augment/rules/ar-coding-standards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# .augment/rules/qs-coding-standards.md
type: always

- Use Go 1.24+ with proper module structure
- Follow qs command patterns in internal/commands/
- Include proper error handling with context.Context
- Use github.com/untillpro/goutils/logger for all logging
7 changes: 7 additions & 0 deletions .augment/rules/ar-github-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# .augment/rules/qs-github-integration.md
type: manual

- Use GitHub CLI (gh) for all GitHub API operations
- Implement retry mechanisms with helper.Retry() for network calls
- Include proper error messages for GitHub authentication failures
- Follow the existing patterns in gitcmds/ package
8 changes: 8 additions & 0 deletions .augment/rules/ar-testing-framework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# .augment/rules/qs-testing-framework.md
type: auto
description: Testing guidelines for qs project

- Use testify framework for unit tests (github.com/stretchr/testify/require)
- Write system tests in sys_test.go for new commands
- Test with real GitHub repositories using systrun framework
- Include both positive and negative test cases
61 changes: 45 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,24 @@ go install github.com/untillpro/qs@latest

## Quick Start

`qs` automatically detects your workflow mode:
- **Single Remote Mode**: When you have direct push access (no upstream remote)
- **Fork Mode**: When contributing to external projects (with upstream remote)

```bash
# Initialize a forked workflow
qs fork # Fork repository and configure remotes
# For repositories with direct access (single remote mode)
qs dev "feature-name" # Create branch - no fork needed!
qs u -m "commit message" # Upload changes
qs pr # Create PR to same repository

# Create a development branch
qs dev # Create branch from clipboard content
qs dev "feature-name" # Create branch with specific name
# For external projects (fork mode)
qs fork # Fork repository and configure remotes
qs dev "feature-name" # Create branch in your fork
qs u -m "commit message" # Upload changes
qs pr # Create PR to upstream

# Work with changes
qs u -m "commit message" # Upload changes (add + commit + push)
# Common commands (work in both modes)
qs d # Download changes (smart sync)

# Create pull request
qs pr # Create PR from current branch
qs pr -d # Create draft PR

# Check status
qs # Show repository status
```

Expand All @@ -82,18 +83,19 @@ qs fork # Fork repository to your account and configure upstr
#### Branch Operations
```bash
qs dev [branch-name] # Create development branch
# - Auto-detects workflow mode (fork vs single remote)
# - Auto-detects branch name from clipboard
# - Supports GitHub issue URLs
# - Supports Jira ticket URLs
# - Links branch to issues automatically
# - Works with or without upstream remote

qs dev -d # Delete merged development branches
# - Removes local and remote branches
# - Only deletes merged branches
# - Cleans up tracking references

qs dev -i, --ignore-hook # Create branch without large file hooks
qs dev -n, --no-fork # Create branch in main repo (no fork required)
```

#### Sync Operations
Expand Down Expand Up @@ -239,7 +241,34 @@ export JIRA_API_TOKEN="your-jira-api-token"

## Workflow Examples

### Standard Fork Workflow
### Single Remote Workflow (Direct Repository Access)
For repositories where you have direct push access and don't need a fork:

```bash
# 1. Clone and work directly
git clone https://github.com/your-org/your-repo.git
cd your-repo

# 2. Create feature branch (no fork needed!)
qs dev "feature-awesome-feature"
# qs automatically detects single remote mode

# 3. Make changes and upload
# ... edit files ...
qs u -m "Add awesome feature"

# 4. Create pull request to the same repository
qs pr

# 5. Clean up after merge
qs dev -d
```

**Note**: `qs` automatically detects single remote mode when you don't have an upstream remote and works seamlessly with just the `origin` remote.

### Fork Workflow (Contributing to External Projects)
For contributing to repositories you don't have direct access to:

```bash
# 1. Fork and setup
git clone https://github.com/original/repo.git
Expand All @@ -253,7 +282,7 @@ qs dev "feature-awesome-feature"
# ... edit files ...
qs u -m "Add awesome feature"

# 4. Create pull request
# 4. Create pull request to upstream
qs pr

# 5. Clean up after merge
Expand Down
47 changes: 39 additions & 8 deletions gitcmds/gitcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -1595,20 +1595,24 @@ func CreateGithubLinkToIssue(wd, parentRepo, githubIssueURL string, issueNumber

// SyncMainBranch syncs the local main branch with upstream and origin
// Flow:
// 1. Pull from UpstreamMain to MainBranch with rebase
// 2. If upstream exists
// - Pull from origin to MainBranch with rebase
// - Push to origin from MainBranch
// 1. If upstream exists: Pull from upstream/main to main with rebase
// 2. Pull from origin/main to main with rebase
// 3. Push to origin/main
// In single remote mode (no upstream), only syncs with origin
func SyncMainBranch(wd string) error {
mainBranch, err := GetMainBranch(wd)
if err != nil {
return fmt.Errorf(errMsgFailedToGetMainBranch, err)
}

// Pull from UpstreamMain to MainBranch with rebase
remoteUpstreamURL := GetRemoteUpstreamURL(wd)
// Check if upstream remote exists
upstreamExists, err := HasRemote(wd, "upstream")
if err != nil {
return fmt.Errorf("failed to check if upstream exists: %w", err)
}

if len(remoteUpstreamURL) > 0 {
// Pull from upstream/main if upstream exists (fork workflow)
if upstreamExists {
stdout, stderr, err := new(exec.PipedExec).
Command(git, pull, "--rebase", "upstream", mainBranch, "--no-edit").
WorkingDir(wd).
Expand Down Expand Up @@ -2350,13 +2354,23 @@ func createPR(

repo := parentRepoName
if len(repo) == 0 {
// Single remote mode: no parent repo, PR to same repo
repo = forkAccount + slash + repoName
}

// Determine the head reference for the PR
// In fork mode: use "forkAccount:branchName" format
// In single remote mode: use just "branchName" format
headRef := prBranchName
if len(parentRepoName) > 0 {
// Fork mode: specify the fork account
headRef = forkAccount + ":" + prBranchName
}

args := []string{
"pr",
"create",
fmt.Sprintf(`--head=%s`, forkAccount+":"+prBranchName),
fmt.Sprintf(`--head=%s`, headRef),
fmt.Sprintf(`--repo=%s`, repo),
fmt.Sprintf(`--body=%s`, strings.TrimSpace(strBody)),
fmt.Sprintf(`--title=%s`, strings.TrimSpace(prTitle)),
Expand Down Expand Up @@ -2798,6 +2812,23 @@ func HasRemote(wd, remoteName string) (bool, error) {
return false, nil
}

// GetEffectiveUpstreamRemote returns the remote to use as upstream.
// If 'upstream' remote exists, returns "upstream".
// Otherwise, returns "origin" (single remote mode).
// This allows qs to work in both fork and non-fork workflows.
func GetEffectiveUpstreamRemote(wd string) (string, error) {
upstreamExists, err := HasRemote(wd, "upstream")
if err != nil {
return "", err
}

if upstreamExists {
return "upstream", nil
}

return "origin", nil
}

func GawkInstalled() bool {
_, _, err := new(exec.PipedExec).
Command("gawk", "--version").
Expand Down
9 changes: 8 additions & 1 deletion gitcmds/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ func Pr(wd string, needDraft bool) error {
// If we are on dev branch than we need to create pr branch
if branchType == notesPkg.BranchTypeDev {
var response string
if len(parentRepoName) > 0 && UpstreamNotExist(wd) {
// Only add upstream if we have a parent repo and upstream doesn't exist
// In single remote mode (no parent repo), we don't need upstream
upstreamExists, err := HasRemote(wd, "upstream")
if err != nil {
return err
}

if len(parentRepoName) > 0 && !upstreamExists {
fmt.Print("Upstream not found.\nRepository " + parentRepoName + " will be added as upstream. Agree[y/n]?")
_, _ = fmt.Scanln(&response)
if response != pushYes {
Expand Down
1 change: 0 additions & 1 deletion internal/cmdproc/cmdproc.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ func devCmd(_ context.Context, params *qsGlobalParams) *cobra.Command {
}
cmd.Flags().BoolP(devDelParamFull, devDelParam, false, devDelMsgComment)
cmd.Flags().BoolP(ignorehookDelParamFull, ignorehookDelParam, false, devIgnoreHookMsgComment)
cmd.Flags().BoolP(noForkParamFull, noForkParam, false, devNoForkMsgComment)

return cmd
}
Expand Down
5 changes: 1 addition & 4 deletions internal/cmdproc/consts.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cmdproc

const (
msgOkSeeYou = "Ok, see you"
msgOkSeeYou = "Ok, see you"
pushParamDesc = "Upload sources to repo"
pushMessageWord = "message"
pushMessageParam = "m"
Expand All @@ -22,14 +22,11 @@ const (
ignorehookDelParamFull = "ignore-hook"
prdraftParam = "d"
prdraftParamFull = "draft"
noForkParam = "n"
noForkParamFull = "no-fork"

prParamDesc = "Make pull request"

devDelMsgComment = "Deletes all merged branches from forked repository"
devIgnoreHookMsgComment = "Ignore creating local hook"
devNoForkMsgComment = "Allows to create branch in main repo"
prdraftMsgComment = "Create draft of pull request"
devParamDesc = "Create developer branch"
upgradeParamDesc = "Print command to upgrade qs"
Expand Down
1 change: 0 additions & 1 deletion internal/commands/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const (
pushYes = "y"

devDelParamFull = "delete"
noForkParamFull = "no-fork"

devConfirm = "Dev branch '$reponame' will be created. Continue(y/n)? "

Expand Down
51 changes: 30 additions & 21 deletions internal/commands/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
"github.com/untillpro/goutils/logger"

"github.com/untillpro/qs/gitcmds"
contextpkg "github.com/untillpro/qs/internal/context"
"github.com/untillpro/qs/internal/helper"
Expand Down Expand Up @@ -45,16 +44,26 @@ func Dev(cmd *cobra.Command, wd string, args []string) error {
args = append(args, clipargs)
}

noForkAllowed := (cmd.Flag(noForkParamFull).Value.String() == trueStr)
if !noForkAllowed {
if len(parentRepo) == 0 { // main repository, not forked
repo, org, err := gitcmds.GetRepoAndOrgName(wd)
if err != nil {
return err
}
// Auto-detect workflow mode:
// - If parentRepo exists OR upstream remote exists -> fork workflow
// - If no parentRepo AND no upstream remote -> single remote workflow
// Check if upstream remote exists
upstreamExists, err := gitcmds.HasRemote(wd, "upstream")
if err != nil {
return err
}

return fmt.Errorf("you are in %s/%s repo\nExecute 'qs fork' first", org, repo)
// Only require fork if:
// 1. No parent repo exists (not a fork), AND
// 2. Upstream remote exists (indicating fork workflow was intended)
// This catches the edge case where someone manually added upstream but didn't fork
if len(parentRepo) == 0 && upstreamExists {
repo, org, err := gitcmds.GetRepoAndOrgName(wd)
if err != nil {
return err
}

return fmt.Errorf("you are in %s/%s repo with upstream remote but no fork detected\nExecute 'qs fork' first", org, repo)
}

curBranch, isMain, err := gitcmds.IamInMainBranch(wd)
Expand Down Expand Up @@ -139,18 +148,18 @@ func Dev(cmd *cobra.Command, wd string, args []string) error {
case pushYes:
// Remote developer branch, linked to issue is created
var response string
if len(parentRepo) > 0 {
if gitcmds.UpstreamNotExist(wd) {
fmt.Print("Upstream not found.\nRepository " + parentRepo + " will be added as upstream. Agree[y/n]?")
_, _ = fmt.Scanln(&response)
if response != pushYes {
fmt.Print(msgOkSeeYou)
return nil
}
response = ""
if err := gitcmds.MakeUpstreamForBranch(wd, parentRepo); err != nil {
return err
}
// Only add upstream if we have a parent repo and upstream doesn't exist
// In single remote mode (no parent repo), we don't need upstream
if len(parentRepo) > 0 && !upstreamExists {
fmt.Print("Upstream not found.\nRepository " + parentRepo + " will be added as upstream. Agree[y/n]?")
_, _ = fmt.Scanln(&response)
if response != pushYes {
fmt.Print(msgOkSeeYou)
return nil
}
response = ""
if err := gitcmds.MakeUpstreamForBranch(wd, parentRepo); err != nil {
return err
}
}

Expand Down
6 changes: 3 additions & 3 deletions internal/systrun/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ type SyncState int
type ClipboardContentType int

const (
// RemoteStateNull means that the remote of the clone repo is null
RemoteStateNull RemoteState = iota
// RemoteStateOK means that remote of the clone repo is configured correctly
RemoteStateOK RemoteState = iota
RemoteStateOK
// RemoteStateMisconfigured means that the remote of the clone repo is not configured correctly,
// e.g. `qs u` should fail on permission error on `git push` (now it does not fail)
RemoteStateMisconfigured
// RemoteStateNull means that the remote of the clone repo is null
RemoteStateNull
)

const (
Expand Down
Loading
Loading