package repostatus import ( "errors" "io/ioutil" "net/http" "os" "path/filepath" "strings" "go.wit.com/lib/gadgets" "go.wit.com/lib/gui/shell" "go.wit.com/log" ) var windowMap map[string]*RepoStatus func init() { windowMap = make(map[string]*RepoStatus) } // deprecate this func ListAllOld() { for path, rs := range windowMap { log.Warn(rs.GetMasterVersion(), path) } } // returns the object for the path // deprecate this func FindPathOld(path string) *RepoStatus { if windowMap[path] == nil { log.Log(INFO, "FindPath() not initialized yet", path) return nil } return windowMap[path] } // makes a window of the status of the repo // don't worry, you can think of it like Sierpinski carpet // it's doesn't need to be displayed so it'll work fine even in an embedded space func New(path string) (*RepoStatus, error) { err, r := NewRepoStatusWindow(path) return r, err } func SetWorkPath(path string) { os.Setenv("REPO_WORK_PATH", path) } // guess the paths. returns // realpath : the actual path on the filesystem // goSrcPath : this could be ~/go/src, or where the go.work file is // goPath : go.wit.com/lib/gui/repostatus (for example) // true/false if the repo is a golang repo func guessPaths(path string) (string, string, string, bool, error) { var realpath, goSrcDir string var isGoLang bool = false homeDir, err := os.UserHomeDir() if err != nil { log.Log(WARN, "Error getting home directory:", err) return path, realpath, goSrcDir, false, err } goSrcDir = filepath.Join(homeDir, "go/src") // allow arbitrary paths, otherwise, assume the repo is in ~/go/src // unless REPO_WORK_PATH was set. to over-ride ~/go/src // todo, look for go.work if os.Getenv("REPO_WORK_PATH") == "" { os.Setenv("REPO_WORK_PATH", goSrcDir) } else { goSrcDir = os.Getenv("REPO_WORK_PATH") } if strings.HasPrefix(path, "/") { realpath = path } else if strings.HasPrefix(path, "~") { tmp := strings.TrimPrefix(path, "~") realpath = filepath.Join(homeDir, tmp) } else { realpath = filepath.Join(goSrcDir, path) isGoLang = true } if os.Getenv("REPO_AUTO_CLONE") == "true" { err := Clone(goSrcDir, path) if err != nil { // directory doesn't exist. exit with nil and error nil return path, realpath, goSrcDir, false, errors.New("git clone") } } if !IsDirectory(realpath) { log.Log(REPOWARN, "directory doesn't exist", realpath) // directory doesn't exist. exit with nil and error nil return path, realpath, goSrcDir, false, errors.New(realpath + " does not exist") } filename := filepath.Join(realpath, ".git/config") _, err = os.Open(filename) if err != nil { // log.Log(WARN, "Error reading .git/config:", filename, err) // log.Log(WARN, "TODO: find .git/config in parent directory") return path, realpath, goSrcDir, false, err } return path, realpath, goSrcDir, isGoLang, nil } // attempt to git clone if the go path doesn't exist func Clone(wdir string, path string) error { fullpath := filepath.Join(wdir, path) if IsDirectory(fullpath) { // directory already exists return nil } err := os.Chdir(wdir) if err != nil { return err } fulldir := filepath.Join(wdir, filepath.Dir(path)) base := filepath.Base(path) os.MkdirAll(fulldir, 0750) err = os.Chdir(fulldir) if err != nil { return err } switch path { case "golang.org/x/crypto": path = "go.googlesource.com/crypto" case "golang.org/x/mod": path = "go.googlesource.com/mod" case "golang.org/x/net": path = "go.googlesource.com/net" case "golang.org/x/sys": path = "go.googlesource.com/sys" case "golang.org/x/sync": path = "go.googlesource.com/sync" case "golang.org/x/term": path = "go.googlesource.com/term" case "golang.org/x/text": path = "go.googlesource.com/text" case "golang.org/x/tools": path = "go.googlesource.com/tools" case "golang.org/x/xerrors": path = "go.googlesource.com/xerrors" } shell.RunPath(fulldir, []string{"git", "clone", "http://" + path}) if IsDirectory(fullpath) { // clone worked return nil } var url string // TODO: attempt to work around things like: // go get golang.org/x/term // is now supposed to be: // git clone https://go.googlesource.com/term // if url, err = findGoImport("http://" + path); err != nil { if path == "golang.org/x/term" { log.Warn("IMPLEMENT git clone hack here. path =", path) log.Warn("IMPLEMENT git clone hack here. path =", path) path = "go.googlesource.com/term" log.Warn("IMPLEMENT git clone hack here. new path =", path) log.Warn("IMPLEMENT git clone hack here. new path =", path) shell.RunPath(fulldir, []string{"git", "clone", "http://" + path}) if IsDirectory(fullpath) { // clone worked return nil } } // this creates a valid URL for git clone if url, err = runGoList(path); err != nil { return err } log.Info("URL:", url) shell.RunPath(fulldir, []string{"git", "clone", url, base}) if IsDirectory(fullpath) { // clone worked return nil } return errors.New("resolve go import") } // this is a terrible hack and really // shouldn't be here. Hopefully this can // be removed and the GO compiler code can // be involked directly (it appears to currently // be an internal/ function in the GO sources // so it can't be imported from outside the compiler func findGoImport(url string) (string, error) { resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } tmp := string(bodyBytes) parts := strings.Split(tmp, "go-import") if len(parts) < 2 { return "", errors.New("missing go-import") } // this is terrible, it doesn't even look for 'content=' // but again, this is just a hack for my own code to be // usuable after the removal in go v1.22 of the go get clone behavior that was in v1.21 parts = strings.Split(parts[1], "\"") var newurl string for { if len(parts) == 0 { break } tmp := strings.TrimSpace(parts[0]) fields := strings.Split(tmp, " ") // log.Info("FIELDS:", fields) if len(fields) == 3 { newurl = fields[2] break } parts = parts[1:] } if newurl == "" { return "", errors.New("missing git content string") } return newurl, nil } func NewRepoStatusWindow(path string) (error, *RepoStatus) { path, realpath, goSrcDir, isGoLang, err := guessPaths(path) if err != nil { return err, nil } if windowMap[path] == nil { log.Log(INFO, "NewRepoStatusWindow() adding new", path) } else { log.Warn("This already exists for path", path) log.Warn("should return windowMap[path] here") return nil, windowMap[path] } rs := &RepoStatus{ ready: false, } rs.tags = make(map[string]string) rs.window = gadgets.RawBasicWindow("GO Repo Details " + path) rs.window.Horizontal() rs.window.Make() basebox := rs.window.Box() group := basebox.NewGroup("stuff") primarybox := group.Box() primarybox.Horizontal() box2 := group.Box() rs.ready = true rs.window.Custom = func() { rs.Hide() log.Warn("repostatus user closed the window()") } // display the status of the git repository rs.drawGitStatus(primarybox) // display the git branches and options rs.makeBranchesBox(primarybox) // show standard git commit and merge controls rs.drawGitCommands(primarybox) // save ~/go/src & the whole path strings rs.path.SetValue(path) rs.goSrcPath.SetValue(goSrcDir) rs.realPath.SetValue(realpath) // add all the tags rs.makeTagBox(box2) rs.readGitConfig() rs.readOnly.SetValue("true") // ignore everything else for now // todo: move this logic to cfgfile.go if strings.HasPrefix(path, "go.wit.com") { rs.readOnly.SetValue("false") } if strings.HasPrefix(path, "git.wit.org") { rs.readOnly.SetValue("false") } // tries 'master', 'main', etc. rs.guessMainWorkingName() // tries 'devel', etc rs.guessDevelWorkingName() // sets this to os.Username rs.setUserWorkingName() if isGoLang { rs.isGoLang.SetText("true") rs.goPath.SetText(path) } windowMap[path] = rs return nil, rs }