зеркало из https://github.com/mislav/hub.git
Merge branch 'update'
This commit is contained in:
Коммит
958169bfac
|
@ -5,6 +5,11 @@
|
||||||
"./..."
|
"./..."
|
||||||
],
|
],
|
||||||
"Deps": [
|
"Deps": [
|
||||||
|
{
|
||||||
|
"ImportPath": "bitbucket.org/kardianos/osext",
|
||||||
|
"Comment": "null-9",
|
||||||
|
"Rev": "364fb577de68fb646c4cb39cc0e09c887ee16376"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/go-netrc/netrc",
|
"ImportPath": "code.google.com/p/go-netrc/netrc",
|
||||||
"Comment": "null-12",
|
"Comment": "null-12",
|
||||||
|
@ -24,10 +29,14 @@
|
||||||
"ImportPath": "github.com/howeyc/gopass",
|
"ImportPath": "github.com/howeyc/gopass",
|
||||||
"Rev": "4cf66881dcc3d6f0071eb6473d95c39e460de696"
|
"Rev": "4cf66881dcc3d6f0071eb6473d95c39e460de696"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/inconshreveable/go-update",
|
||||||
|
"Rev": "3f0466666779bd2143f368a207b0641f0ed536e8"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/jingweno/go-octokit/octokit",
|
"ImportPath": "github.com/jingweno/go-octokit/octokit",
|
||||||
"Comment": "v0.4.0-27-g1dbe02b",
|
"Comment": "v0.4.0-41-g85bc6b5",
|
||||||
"Rev": "1dbe02be6d8bd53eaf3869d97b1746ff82e2a807"
|
"Rev": "85bc6b536f8e8cb24d4db262e4dfea3d7d2d8138"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/jtacoma/uritemplates",
|
"ImportPath": "github.com/jtacoma/uritemplates",
|
||||||
|
|
20
Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE
сгенерированный
поставляемый
Normal file
20
Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2012 Daniel Theophanes
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
|
||||||
|
3. This notice may not be removed or altered from any source
|
||||||
|
distribution.
|
32
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go
сгенерированный
поставляемый
Normal file
32
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Extensions to the standard "os" package.
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
// Executable returns an absolute path that can be used to
|
||||||
|
// re-invoke the current program.
|
||||||
|
// It may not be valid after the current program exits.
|
||||||
|
func Executable() (string, error) {
|
||||||
|
p, err := executable()
|
||||||
|
return filepath.Clean(p), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns same path as Executable, returns just the folder
|
||||||
|
// path. Excludes the executable name.
|
||||||
|
func ExecutableFolder() (string, error) {
|
||||||
|
p, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
folder, _ := filepath.Split(p)
|
||||||
|
return folder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depricated. Same as Executable().
|
||||||
|
func GetExePath() (exePath string, err error) {
|
||||||
|
return Executable()
|
||||||
|
}
|
16
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go
сгенерированный
поставляемый
Normal file
16
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
f, err := Open("/proc/" + itoa(Getpid()) + "/text")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return syscall.Fd2path(int(f.Fd()))
|
||||||
|
}
|
25
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go
сгенерированный
поставляемый
Normal file
25
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build linux netbsd openbsd
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
return os.Readlink("/proc/self/exe")
|
||||||
|
case "netbsd":
|
||||||
|
return os.Readlink("/proc/curproc/exe")
|
||||||
|
case "openbsd":
|
||||||
|
return os.Readlink("/proc/curproc/file")
|
||||||
|
}
|
||||||
|
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
|
||||||
|
}
|
82
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go
сгенерированный
поставляемый
Normal file
82
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin freebsd
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var startUpcwd, getwdError = os.Getwd()
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
var mib [4]int32
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
|
||||||
|
case "darwin":
|
||||||
|
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
n := uintptr(0)
|
||||||
|
// get length
|
||||||
|
_, _, err := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||||
|
if err != 0 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if n == 0 { // shouldn't happen
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
buf := make([]byte, n)
|
||||||
|
_, _, err = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||||
|
if err != 0 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if n == 0 { // shouldn't happen
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
for i, v := range buf {
|
||||||
|
if v == 0 {
|
||||||
|
buf = buf[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var strpath string
|
||||||
|
if buf[0] != '/' {
|
||||||
|
var e error
|
||||||
|
if strpath, e = getAbs(buf); e != nil {
|
||||||
|
return strpath, e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strpath = string(buf)
|
||||||
|
}
|
||||||
|
// darwin KERN_PROCARGS may return the path to a symlink rather than the
|
||||||
|
// actual executable
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
if strpath, err := filepath.EvalSymlinks(strpath); err != nil {
|
||||||
|
return strpath, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strpath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAbs(buf []byte) (string, error) {
|
||||||
|
if getwdError != nil {
|
||||||
|
return string(buf), getwdError
|
||||||
|
} else {
|
||||||
|
if buf[0] == '.' {
|
||||||
|
buf = buf[1:]
|
||||||
|
}
|
||||||
|
if startUpcwd[len(startUpcwd)-1] != '/' && buf[0] != '/' {
|
||||||
|
return startUpcwd + "/" + string(buf), nil
|
||||||
|
}
|
||||||
|
return startUpcwd + string(buf), nil
|
||||||
|
}
|
||||||
|
}
|
79
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go
сгенерированный
поставляемый
Normal file
79
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin linux freebsd netbsd windows
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
oexec "os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const execPath_EnvVar = "OSTEST_OUTPUT_EXECPATH"
|
||||||
|
|
||||||
|
func TestExecPath(t *testing.T) {
|
||||||
|
ep, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecPath failed: %v", err)
|
||||||
|
}
|
||||||
|
// we want fn to be of the form "dir/prog"
|
||||||
|
dir := filepath.Dir(filepath.Dir(ep))
|
||||||
|
fn, err := filepath.Rel(dir, ep)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("filepath.Rel: %v", err)
|
||||||
|
}
|
||||||
|
cmd := &oexec.Cmd{}
|
||||||
|
// make child start with a relative program path
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Path = fn
|
||||||
|
// forge argv[0] for child, so that we can verify we could correctly
|
||||||
|
// get real path of the executable without influenced by argv[0].
|
||||||
|
cmd.Args = []string{"-", "-test.run=XXXX"}
|
||||||
|
cmd.Env = []string{fmt.Sprintf("%s=1", execPath_EnvVar)}
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec(self) failed: %v", err)
|
||||||
|
}
|
||||||
|
outs := string(out)
|
||||||
|
if !filepath.IsAbs(outs) {
|
||||||
|
t.Fatalf("Child returned %q, want an absolute path", out)
|
||||||
|
}
|
||||||
|
if !sameFile(outs, ep) {
|
||||||
|
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameFile(fn1, fn2 string) bool {
|
||||||
|
fi1, err := os.Stat(fn1)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fi2, err := os.Stat(fn2)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return os.SameFile(fi1, fi2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if e := os.Getenv(execPath_EnvVar); e != "" {
|
||||||
|
// first chdir to another path
|
||||||
|
dir := "/"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
dir = filepath.VolumeName(".")
|
||||||
|
}
|
||||||
|
os.Chdir(dir)
|
||||||
|
if ep, err := Executable(); err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(os.Stderr, ep)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
34
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go
сгенерированный
поставляемый
Normal file
34
Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel = syscall.MustLoadDLL("kernel32.dll")
|
||||||
|
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetModuleFileName() with hModule = NULL
|
||||||
|
func executable() (exePath string, err error) {
|
||||||
|
return getModuleFileName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModuleFileName() (string, error) {
|
||||||
|
var n uint32
|
||||||
|
b := make([]uint16, syscall.MAX_PATH)
|
||||||
|
size := uint32(len(b))
|
||||||
|
|
||||||
|
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
|
||||||
|
n = uint32(r0)
|
||||||
|
if n == 0 {
|
||||||
|
return "", e1
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(b[0:n])), nil
|
||||||
|
}
|
137
Godeps/_workspace/src/github.com/inconshreveable/go-update/README
сгенерированный
поставляемый
Normal file
137
Godeps/_workspace/src/github.com/inconshreveable/go-update/README
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
PACKAGE DOCUMENTATION
|
||||||
|
|
||||||
|
package update
|
||||||
|
import "github.com/inconshreveable/go-update"
|
||||||
|
|
||||||
|
Package update allows a program to "self-update", replacing its
|
||||||
|
executable file with new bytes.
|
||||||
|
|
||||||
|
Package update provides the facility to create user experiences like
|
||||||
|
auto-updating or user-approved updates which manifest as user prompts in
|
||||||
|
commercial applications with copy similar to "Restart to being using the
|
||||||
|
new version of X".
|
||||||
|
|
||||||
|
Updating your program to a new version is as easy as:
|
||||||
|
|
||||||
|
err := update.FromUrl("http://release.example.com/2.0/myprogram")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Update failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
The most low-level API is FromStream() which updates the current
|
||||||
|
executable with the bytes read from an io.Reader.
|
||||||
|
|
||||||
|
Additional APIs are provided for common update strategies which include
|
||||||
|
updating from a file with FromFile() and updating from the internet with
|
||||||
|
FromUrl().
|
||||||
|
|
||||||
|
Using the more advaced Download.UpdateFromUrl() API gives you the
|
||||||
|
ability to resume an interrupted download to enable large updates to
|
||||||
|
complete even over intermittent or slow connections. This API also
|
||||||
|
enables more fine-grained control over how the update is downloaded from
|
||||||
|
the internet as well as access to download progress,
|
||||||
|
|
||||||
|
|
||||||
|
VARIABLES
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Returned when the remote server indicates that no download is available
|
||||||
|
UpdateUnavailable error
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTIONS
|
||||||
|
|
||||||
|
func FromFile(filepath string) (err error)
|
||||||
|
FromFile reads the contents of the given file and uses them to update
|
||||||
|
the current program's executable file by calling FromStream().
|
||||||
|
|
||||||
|
func FromStream(newBinary io.Reader) (err error)
|
||||||
|
FromStream reads the contents of the supplied io.Reader newBinary and
|
||||||
|
uses them to update the current program's executable file.
|
||||||
|
|
||||||
|
FromStream performs the following actions to ensure a cross-platform
|
||||||
|
safe update:
|
||||||
|
|
||||||
|
- Renames the current program's executable file from
|
||||||
|
/path/to/program-name to /path/to/.program-name.old
|
||||||
|
|
||||||
|
- Opens the now-empty path /path/to/program-name with mode 0755 and
|
||||||
|
copies the contents of newBinary into the file.
|
||||||
|
|
||||||
|
- If the copy is successful, it erases /path/to/.program.old. If this
|
||||||
|
operation fails, no error is reported.
|
||||||
|
|
||||||
|
- If the copy is unsuccessful, it attempts to rename
|
||||||
|
/path/to/.program-name.old back to /path/to/program-name. If this
|
||||||
|
operation fails, the error is not reported in order to not mask the
|
||||||
|
error that caused the rename recovery attempt.
|
||||||
|
|
||||||
|
func FromUrl(url string) error
|
||||||
|
FromUrl downloads the contents of the given url and uses them to update
|
||||||
|
the current program's executable file. It is a convenience function
|
||||||
|
which is equivalent to
|
||||||
|
|
||||||
|
NewDownload().UpdateFromUrl(url)
|
||||||
|
|
||||||
|
See Download.UpdateFromUrl for more details.
|
||||||
|
|
||||||
|
|
||||||
|
TYPES
|
||||||
|
|
||||||
|
type Download struct {
|
||||||
|
// net/http.Client to use when downloading the update.
|
||||||
|
// If nil, a default http.Client is used
|
||||||
|
HttpClient *http.Client
|
||||||
|
|
||||||
|
// Path on the file system to dowload the update to
|
||||||
|
// If empty, a temporary file is used.
|
||||||
|
// After the download begins, this path will be set
|
||||||
|
// so that the client can use it to resume aborted
|
||||||
|
// downloads
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// Progress returns the percentage of the download
|
||||||
|
// completed as an integer between 0 and 100
|
||||||
|
Progress chan (int)
|
||||||
|
|
||||||
|
// HTTP Method to use in the download request. Default is "GET"
|
||||||
|
Method string
|
||||||
|
}
|
||||||
|
Type Download encapsulates the necessary parameters and state needed to
|
||||||
|
download an update from the internet. Create an instance with the
|
||||||
|
NewDownload() factory function.
|
||||||
|
|
||||||
|
|
||||||
|
func NewDownload() *Download
|
||||||
|
NewDownload initializes a new Download object
|
||||||
|
|
||||||
|
|
||||||
|
func (d *Download) UpdateFromUrl(url string) (err error)
|
||||||
|
UpdateFromUrl downloads the given url from the internet to a file on
|
||||||
|
disk and then calls FromStream() to update the current program's
|
||||||
|
executable file with the contents of that file.
|
||||||
|
|
||||||
|
If the update is successful, the downloaded file will be erased from
|
||||||
|
disk. Otherwise, it will remain in d.Path to allow the download to
|
||||||
|
resume later or be skipped entirely.
|
||||||
|
|
||||||
|
Only HTTP/1.1 servers that implement the Range header are supported.
|
||||||
|
|
||||||
|
UpdateFromUrl() uses HTTP status codes to determine what action to take.
|
||||||
|
|
||||||
|
- The HTTP server should return 200 or 206 for the update to be
|
||||||
|
downloaded.
|
||||||
|
|
||||||
|
- The HTTP server should return 204 if no update is available at this
|
||||||
|
time. This will cause UpdateFromUrl to return the error
|
||||||
|
UpdateUnavailable.
|
||||||
|
|
||||||
|
- If the HTTP server returns a 3XX redirect, it will be followed
|
||||||
|
according to d.HttpClient's redirect policy.
|
||||||
|
|
||||||
|
- Any other HTTP status code will cause UpdateFromUrl to return an
|
||||||
|
error.
|
||||||
|
|
||||||
|
|
||||||
|
|
455
Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go
сгенерированный
поставляемый
Normal file
455
Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
/*
|
||||||
|
Package update allows a program to "self-update", replacing its executable file
|
||||||
|
with new bytes.
|
||||||
|
|
||||||
|
Package update provides the facility to create user experiences like auto-updating
|
||||||
|
or user-approved updates which manifest as user prompts in commercial applications
|
||||||
|
with copy similar to "Restart to being using the new version of X".
|
||||||
|
|
||||||
|
Updating your program to a new version is as easy as:
|
||||||
|
|
||||||
|
err := update.FromUrl("http://release.example.com/2.0/myprogram")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Update failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
The most low-level API is FromStream() which updates the current executable
|
||||||
|
with the bytes read from an io.Reader.
|
||||||
|
|
||||||
|
Additional APIs are provided for common update strategies which include
|
||||||
|
updating from a file with FromFile() and updating from the internet with
|
||||||
|
FromUrl().
|
||||||
|
|
||||||
|
Using the more advaced Download.UpdateFromUrl() API gives you the ability
|
||||||
|
to resume an interrupted download to enable large updates to complete even
|
||||||
|
over intermittent or slow connections. This API also enables more fine-grained
|
||||||
|
control over how the update is downloaded from the internet as well as access to
|
||||||
|
download progress,
|
||||||
|
*/
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"bitbucket.org/kardianos/osext"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MeteredReader struct {
|
||||||
|
rd io.ReadCloser
|
||||||
|
totalSize int64
|
||||||
|
progress chan int
|
||||||
|
totalRead int64
|
||||||
|
ticks int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MeteredReader) Close() error {
|
||||||
|
return m.rd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MeteredReader) Read(b []byte) (n int, err error) {
|
||||||
|
chunkSize := (m.totalSize / 100) + 1
|
||||||
|
lenB := int64(len(b))
|
||||||
|
|
||||||
|
var nChunk int
|
||||||
|
for start := int64(0); start < lenB; start += int64(nChunk) {
|
||||||
|
end := start + chunkSize
|
||||||
|
if end > lenB {
|
||||||
|
end = lenB
|
||||||
|
}
|
||||||
|
|
||||||
|
nChunk, err = m.rd.Read(b[start:end])
|
||||||
|
|
||||||
|
n += nChunk
|
||||||
|
m.totalRead += int64(nChunk)
|
||||||
|
|
||||||
|
if m.totalRead > (m.ticks * chunkSize) {
|
||||||
|
m.ticks += 1
|
||||||
|
// try to send on channel, but don't block if it's full
|
||||||
|
select {
|
||||||
|
case m.progress <- int(m.ticks + 1):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// give the progress channel consumer a chance to run
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wrap the round tripper when making requests
|
||||||
|
// because we need to add headers to the requests we make
|
||||||
|
// even when they are requests made after a redirect
|
||||||
|
type RoundTripper struct {
|
||||||
|
RoundTripFn func(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
return rt.RoundTripFn(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type Download encapsulates the necessary parameters and state
|
||||||
|
// needed to download an update from the internet. Create an instance
|
||||||
|
// with the NewDownload() factory function.
|
||||||
|
//
|
||||||
|
// You may only use a Download once,
|
||||||
|
type Download struct {
|
||||||
|
// net/http.Client to use when downloading the update.
|
||||||
|
// If nil, a default http.Client is used
|
||||||
|
HttpClient *http.Client
|
||||||
|
|
||||||
|
// Path on the file system to dowload the update to
|
||||||
|
// If empty, a temporary file is used.
|
||||||
|
// After the download begins, this path will be set
|
||||||
|
// so that the client can use it to resume aborted
|
||||||
|
// downloads
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// Progress returns the percentage of the download
|
||||||
|
// completed as an integer between 0 and 100
|
||||||
|
Progress chan (int)
|
||||||
|
|
||||||
|
// HTTP Method to use in the download request. Default is "GET"
|
||||||
|
Method string
|
||||||
|
|
||||||
|
// HTTP URL to issue the download request to
|
||||||
|
Url string
|
||||||
|
|
||||||
|
// Set to true when the server confirms a new version is available
|
||||||
|
// even if the updating process encounters an error later on
|
||||||
|
Available bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDownload initializes a new Download object
|
||||||
|
func NewDownload(url string) *Download {
|
||||||
|
return &Download{
|
||||||
|
HttpClient: new(http.Client),
|
||||||
|
Progress: make(chan int),
|
||||||
|
Method: "GET",
|
||||||
|
Url: url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Download) sharedHttp(offset int64) (resp *http.Response, err error) {
|
||||||
|
// create the download request
|
||||||
|
req, err := http.NewRequest(d.Method, d.Url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to add headers like this so they get used across redirects
|
||||||
|
trans := d.HttpClient.Transport
|
||||||
|
if trans == nil {
|
||||||
|
trans = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
d.HttpClient.Transport = &RoundTripper{
|
||||||
|
RoundTripFn: func(r *http.Request) (*http.Response, error) {
|
||||||
|
// add header for download continuation
|
||||||
|
if offset > 0 {
|
||||||
|
r.Header.Add("Range", fmt.Sprintf("%d-", offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask for gzipped content so that net/http won't unzip it for us
|
||||||
|
// and destroy the content length header we need for progress calculations
|
||||||
|
r.Header.Add("Accept-Encoding", "gzip")
|
||||||
|
|
||||||
|
return trans.RoundTrip(r)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// issue the download request
|
||||||
|
return d.HttpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Download) Check() (available bool, err error) {
|
||||||
|
resp, err := d.sharedHttp(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
// ok
|
||||||
|
case 200, 206:
|
||||||
|
available = true
|
||||||
|
|
||||||
|
// no update available
|
||||||
|
case 204:
|
||||||
|
available = false
|
||||||
|
|
||||||
|
// server error
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Non 2XX response when downloading update: %s", resp.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get() downloads the given url from the internet to a file on disk
|
||||||
|
// and then calls FromStream() to update the current program's executable file
|
||||||
|
// with the contents of that file.
|
||||||
|
//
|
||||||
|
// If the update is successful, the downloaded file will be erased from disk.
|
||||||
|
// Otherwise, it will remain in d.Path to allow the download to resume later
|
||||||
|
// or be skipped entirely.
|
||||||
|
//
|
||||||
|
// Only HTTP/1.1 servers that implement the Range header support resuming a
|
||||||
|
// partially completed download.
|
||||||
|
//
|
||||||
|
// UpdateFromUrl() uses HTTP status codes to determine what action to take.
|
||||||
|
//
|
||||||
|
// - The HTTP server should return 200 or 206 for the update to be downloaded.
|
||||||
|
//
|
||||||
|
// - The HTTP server should return 204 if no update is available at this time.
|
||||||
|
//
|
||||||
|
// - If the HTTP server returns a 3XX redirect, it will be followed
|
||||||
|
// according to d.HttpClient's redirect policy.
|
||||||
|
//
|
||||||
|
// - Any other HTTP status code will cause UpdateFromUrl to return an error.
|
||||||
|
func (d *Download) Get() (err error) {
|
||||||
|
var offset int64 = 0
|
||||||
|
var fp *os.File
|
||||||
|
|
||||||
|
// Close the progress channel whenever this function completes
|
||||||
|
defer close(d.Progress)
|
||||||
|
|
||||||
|
// open a file where we will stream the downloaded update to
|
||||||
|
// we do this first because if the caller specified a non-empty dlpath
|
||||||
|
// we need to determine how large it is in order to resume the download
|
||||||
|
if d.Path == "" {
|
||||||
|
// no dlpath specified, use a random tempfile
|
||||||
|
fp, err = ioutil.TempFile("", "update")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
// remember the path
|
||||||
|
d.Path = fp.Name()
|
||||||
|
} else {
|
||||||
|
fp, err = os.OpenFile(d.Path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
// determine the file size so we can resume the download, if possible
|
||||||
|
var fi os.FileInfo
|
||||||
|
fi, err = fp.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = fi.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// start downloading the file
|
||||||
|
resp, err := d.sharedHttp(offset)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
// ok
|
||||||
|
case 200, 206:
|
||||||
|
d.Available = true
|
||||||
|
|
||||||
|
// no update available
|
||||||
|
case 204:
|
||||||
|
return
|
||||||
|
|
||||||
|
// server error
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Non 2XX response when downloading update: %s", resp.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine how much we have to download
|
||||||
|
// net/http sets this to -1 when it is unknown
|
||||||
|
clength := resp.ContentLength
|
||||||
|
|
||||||
|
// Read the content from the response body
|
||||||
|
rd := resp.Body
|
||||||
|
|
||||||
|
// meter the rate at which we download content for
|
||||||
|
// progress reporting if we know how much to expect
|
||||||
|
if clength > 0 {
|
||||||
|
rd = &MeteredReader{rd: rd, totalSize: clength, progress: d.Progress}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress the content if necessary
|
||||||
|
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
rd, err = gzip.NewReader(rd)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the update
|
||||||
|
_, err = io.Copy(fp, rd)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Download) GetAndUpdate() (err error, errRecover error) {
|
||||||
|
// check before we download if this will work
|
||||||
|
if err = SanityCheck(); err != nil {
|
||||||
|
// keep the contract that d.Progress will close whenever Get() terminates
|
||||||
|
close(d.Progress)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// download the update
|
||||||
|
if err = d.Get(); err != nil || !d.Available {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply the update
|
||||||
|
if err, errRecover = FromFile(d.Path); err != nil || errRecover != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the temporary file
|
||||||
|
os.Remove(d.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromUrl downloads the contents of the given url and uses them to update
|
||||||
|
// the current program's executable file. It is a convenience function which is equivalent to
|
||||||
|
//
|
||||||
|
// NewDownload(url).GetAndUpdate()
|
||||||
|
//
|
||||||
|
// See Download.Get() for more details.
|
||||||
|
func FromUrl(url string) (err error, errRecover error) {
|
||||||
|
return NewDownload(url).GetAndUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromFile reads the contents of the given file and uses them
|
||||||
|
// to update the current program's executable file by calling FromStream().
|
||||||
|
func FromFile(filepath string) (err error, errRecover error) {
|
||||||
|
// open the new binary
|
||||||
|
fp, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
// do the update
|
||||||
|
return FromStream(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromStream reads the contents of the supplied io.Reader newBinary
|
||||||
|
// and uses them to update the current program's executable file.
|
||||||
|
//
|
||||||
|
// FromStream performs the following actions to ensure a cross-platform safe
|
||||||
|
// update:
|
||||||
|
//
|
||||||
|
// - Creates a new file, /path/to/.program-name.new with mode 0755 and copies
|
||||||
|
// the contents of newBinary into the file
|
||||||
|
//
|
||||||
|
// - Renames the current program's executable file from /path/to/program-name
|
||||||
|
// to /path/to/.program-name.old
|
||||||
|
//
|
||||||
|
// - Renames /path/to/.program-name.new to /path/to/program-name
|
||||||
|
//
|
||||||
|
// - If the rename is successful, it erases /path/to/.program.old. If this operation
|
||||||
|
// fails, no error is reported.
|
||||||
|
//
|
||||||
|
// - If the rename is unsuccessful, it attempts to rename /path/to/.program-name.old
|
||||||
|
// back to /path/to/program-name. If this operation fails, the error is not reported
|
||||||
|
// in order to not mask the error that caused the rename recovery attempt.
|
||||||
|
func FromStream(newBinary io.Reader) (err error, errRecover error) {
|
||||||
|
// get the path to the executable
|
||||||
|
thisExecPath, err := osext.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the directory the executable exists in
|
||||||
|
execDir := filepath.Dir(thisExecPath)
|
||||||
|
execName := filepath.Base(thisExecPath)
|
||||||
|
|
||||||
|
// Copy the contents of of newbinary to a the new executable file
|
||||||
|
newExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.new", execName))
|
||||||
|
fp, err := os.OpenFile(newExecPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
_, err = io.Copy(fp, newBinary)
|
||||||
|
|
||||||
|
// if we don't call fp.Close(), windows won't let us move the new executable
|
||||||
|
// because the file will still be "in use"
|
||||||
|
fp.Close()
|
||||||
|
|
||||||
|
// this is where we'll move the executable to so that we can swap in the updated replacement
|
||||||
|
oldExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.old", execName))
|
||||||
|
|
||||||
|
// delete any existing old exec file - this is necessary on Windows for two reasons:
|
||||||
|
// 1. after a successful update, windows can't remove the .old file because the process is still running
|
||||||
|
// 2. windows rename operations fail if the destination file already exists
|
||||||
|
_ = os.Remove(oldExecPath)
|
||||||
|
|
||||||
|
// move the existing executable to a new file in the same directory
|
||||||
|
err = os.Rename(thisExecPath, oldExecPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the new exectuable in to become the new program
|
||||||
|
err = os.Rename(newExecPath, thisExecPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// copy unsuccessful
|
||||||
|
errRecover = os.Rename(oldExecPath, thisExecPath)
|
||||||
|
} else {
|
||||||
|
// copy successful, remove the old binary
|
||||||
|
_ = os.Remove(oldExecPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanityCheck() attempts to determine whether an in-place executable update could
|
||||||
|
// succeed by performing preliminary checks (to establish valid permissions, etc).
|
||||||
|
// This helps avoid downloading updates when we know the update can't be successfully
|
||||||
|
// applied later.
|
||||||
|
func SanityCheck() (err error) {
|
||||||
|
// get the path to the executable
|
||||||
|
thisExecPath, err := osext.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the directory the executable exists in
|
||||||
|
execDir := filepath.Dir(thisExecPath)
|
||||||
|
execName := filepath.Base(thisExecPath)
|
||||||
|
|
||||||
|
// attempt to open a file in the executable's directory
|
||||||
|
newExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.new", execName))
|
||||||
|
fp, err := os.OpenFile(newExecPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fp.Close()
|
||||||
|
|
||||||
|
_ = os.Remove(newExecPath)
|
||||||
|
return
|
||||||
|
}
|
68
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go
сгенерированный
поставляемый
68
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/client.go
сгенерированный
поставляемый
|
@ -44,65 +44,49 @@ func (c *Client) NewRequest(urlStr string) (req *Request, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) head(url *url.URL, output interface{}) (result *Result) {
|
func (c *Client) head(url *url.URL, output interface{}) (result *Result) {
|
||||||
req, err := c.NewRequest(url.String())
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
if err != nil {
|
return req.Head(output)
|
||||||
result = newResult(nil, err)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := req.Head(output)
|
|
||||||
result = newResult(resp, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) get(url *url.URL, output interface{}) (result *Result) {
|
func (c *Client) get(url *url.URL, output interface{}) (result *Result) {
|
||||||
req, err := c.NewRequest(url.String())
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
if err != nil {
|
return req.Get(output)
|
||||||
result = newResult(nil, err)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := req.Get(output)
|
|
||||||
result = newResult(resp, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) post(url *url.URL, input interface{}, output interface{}) (result *Result) {
|
func (c *Client) post(url *url.URL, input interface{}, output interface{}) (result *Result) {
|
||||||
req, err := c.NewRequest(url.String())
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
if err != nil {
|
return req.Post(input, output)
|
||||||
result = newResult(nil, err)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := req.Post(input, output)
|
|
||||||
result = newResult(resp, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) put(url *url.URL, input interface{}, output interface{}) (result *Result) {
|
func (c *Client) put(url *url.URL, input interface{}, output interface{}) *Result {
|
||||||
req, err := c.NewRequest(url.String())
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
if err != nil {
|
return req.Put(input, output)
|
||||||
result = newResult(nil, err)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := req.Put(input, output)
|
|
||||||
result = newResult(resp, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) delete(url *url.URL, output interface{}) (result *Result) {
|
func (c *Client) delete(url *url.URL, output interface{}) (result *Result) {
|
||||||
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
|
return req.Delete(output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) patch(url *url.URL, input interface{}, output interface{}) (result *Result) {
|
||||||
|
return sendRequest(c, url, func(req *Request) (*Response, error) {
|
||||||
|
return req.Patch(input, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendRequest(c *Client, url *url.URL, fn func(r *Request) (*Response, error)) (result *Result) {
|
||||||
req, err := c.NewRequest(url.String())
|
req, err := c.NewRequest(url.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = newResult(nil, err)
|
result = newResult(nil, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := req.Delete(output)
|
resp, err := fn(req)
|
||||||
result = newResult(resp, err)
|
result = newResult(resp, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
19
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/issues.go
сгенерированный
поставляемый
19
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/issues.go
сгенерированный
поставляемый
|
@ -31,6 +31,16 @@ func (i *IssuesService) All() (issues []Issue, result *Result) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *IssuesService) Create(params interface{}) (issue *Issue, result *Result) {
|
||||||
|
result = i.client.post(i.URL, params, &issue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssuesService) Update(params interface{}) (issue *Issue, result *Result) {
|
||||||
|
result = i.client.patch(i.URL, params, &issue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
*hypermedia.HALResource
|
*hypermedia.HALResource
|
||||||
|
|
||||||
|
@ -69,3 +79,12 @@ type Issue struct {
|
||||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IssueParams struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
Assignee string `json:"assignee,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
Milestone uint64 `json:"milestone,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
}
|
||||||
|
|
46
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/issues_test.go
сгенерированный
поставляемый
46
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/issues_test.go
сгенерированный
поставляемый
|
@ -45,6 +45,52 @@ func TestIssuesService_One(t *testing.T) {
|
||||||
validateIssue(t, *issue)
|
validateIssue(t, *issue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssuesService_Create(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/repos/octocat/Hello-World/issues", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "POST")
|
||||||
|
testBody(t, r, "{\"title\":\"title\",\"body\":\"body\"}\n")
|
||||||
|
respondWithJSON(w, loadFixture("issue.json"))
|
||||||
|
})
|
||||||
|
|
||||||
|
url, err := RepoIssuesURL.Expand(M{"owner": "octocat", "repo": "Hello-World"})
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
params := IssueParams{
|
||||||
|
Title: "title",
|
||||||
|
Body: "body",
|
||||||
|
}
|
||||||
|
issue, result := client.Issues(url).Create(params)
|
||||||
|
|
||||||
|
assert.T(t, !result.HasError())
|
||||||
|
validateIssue(t, *issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssuesService_Update(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/repos/octocat/Hello-World/issues/1347", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "PATCH")
|
||||||
|
testBody(t, r, "{\"title\":\"title\",\"body\":\"body\"}\n")
|
||||||
|
respondWithJSON(w, loadFixture("issue.json"))
|
||||||
|
})
|
||||||
|
|
||||||
|
url, err := RepoIssuesURL.Expand(M{"owner": "octocat", "repo": "Hello-World", "number": 1347})
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
params := IssueParams{
|
||||||
|
Title: "title",
|
||||||
|
Body: "body",
|
||||||
|
}
|
||||||
|
issue, result := client.Issues(url).Update(params)
|
||||||
|
|
||||||
|
assert.T(t, !result.HasError())
|
||||||
|
validateIssue(t, *issue)
|
||||||
|
}
|
||||||
|
|
||||||
func validateIssue(t *testing.T, issue Issue) {
|
func validateIssue(t *testing.T, issue Issue) {
|
||||||
|
|
||||||
assert.Equal(t, "https://api.github.com/repos/octocat/Hello-World/issues/1347", issue.URL)
|
assert.Equal(t, "https://api.github.com/repos/octocat/Hello-World/issues/1347", issue.URL)
|
||||||
|
|
5
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go
сгенерированный
поставляемый
5
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases.go
сгенерированный
поставляемый
|
@ -63,6 +63,11 @@ func (r *ReleasesService) Create(params interface{}) (release *Release, result *
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ReleasesService) Update(params interface{}) (release *Release, result *Result) {
|
||||||
|
result = r.client.patch(r.URL, params, &release)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type ReleaseParams struct {
|
type ReleaseParams struct {
|
||||||
TagName string `json:"tag_name,omitempty"`
|
TagName string `json:"tag_name,omitempty"`
|
||||||
TargetCommitish string `json:"target_commitish,omitempty"`
|
TargetCommitish string `json:"target_commitish,omitempty"`
|
||||||
|
|
23
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go
сгенерированный
поставляемый
23
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/releases_test.go
сгенерированный
поставляемый
|
@ -75,3 +75,26 @@ func TestCreateRelease(t *testing.T) {
|
||||||
assert.T(t, !result.HasError())
|
assert.T(t, !result.HasError())
|
||||||
assert.Equal(t, "v1.0.0", release.TagName)
|
assert.Equal(t, "v1.0.0", release.TagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateRelease(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/repos/octokit/Hello-World/releases/123", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "PATCH")
|
||||||
|
testBody(t, r, "{\"tag_name\":\"v1.0.0\",\"target_commitish\":\"master\"}\n")
|
||||||
|
respondWithJSON(w, loadFixture("create_release.json"))
|
||||||
|
})
|
||||||
|
|
||||||
|
url, err := ReleasesURL.Expand(M{"owner": "octokit", "repo": "Hello-World", "id": "123"})
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
params := Release{
|
||||||
|
TagName: "v1.0.0",
|
||||||
|
TargetCommitish: "master",
|
||||||
|
}
|
||||||
|
release, result := client.Releases(url).Update(params)
|
||||||
|
|
||||||
|
assert.T(t, !result.HasError())
|
||||||
|
assert.Equal(t, "v1.0.0", release.TagName)
|
||||||
|
}
|
||||||
|
|
17
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go
сгенерированный
поставляемый
17
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/request.go
сгенерированный
поставляемый
|
@ -10,27 +10,32 @@ type Request struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Head(output interface{}) (resp *Response, err error) {
|
func (r *Request) Head(output interface{}) (resp *Response, err error) {
|
||||||
resp, err = r.do("HEAD", nil, output)
|
resp, err = r.do(sawyer.HeadMethod, nil, output)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Get(output interface{}) (resp *Response, err error) {
|
func (r *Request) Get(output interface{}) (resp *Response, err error) {
|
||||||
resp, err = r.do("GET", nil, output)
|
resp, err = r.do(sawyer.GetMethod, nil, output)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Post(input interface{}, output interface{}) (resp *Response, err error) {
|
func (r *Request) Post(input interface{}, output interface{}) (resp *Response, err error) {
|
||||||
resp, err = r.do("POST", input, output)
|
resp, err = r.do(sawyer.PostMethod, input, output)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Put(input interface{}, output interface{}) (resp *Response, err error) {
|
func (r *Request) Put(input interface{}, output interface{}) (resp *Response, err error) {
|
||||||
resp, err = r.do("PUT", input, output)
|
resp, err = r.do(sawyer.PutMethod, input, output)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Delete(output interface{}) (resp *Response, err error) {
|
func (r *Request) Delete(output interface{}) (resp *Response, err error) {
|
||||||
resp, err = r.do("DELETE", nil, output)
|
resp, err = r.do(sawyer.DeleteMethod, nil, output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Patch(input interface{}, output interface{}) (resp *Response, err error) {
|
||||||
|
resp, err = r.do(sawyer.PatchMethod, input, output)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +55,8 @@ func (r *Request) do(method string, input interface{}, output interface{}) (resp
|
||||||
r.sawyerReq.SetBody(mtype, input)
|
r.sawyerReq.SetBody(mtype, input)
|
||||||
sawyerResp = r.sawyerReq.Put()
|
sawyerResp = r.sawyerReq.Put()
|
||||||
case sawyer.PatchMethod:
|
case sawyer.PatchMethod:
|
||||||
|
mtype, _ := mediatype.Parse(defaultMediaType)
|
||||||
|
r.sawyerReq.SetBody(mtype, input)
|
||||||
sawyerResp = r.sawyerReq.Patch()
|
sawyerResp = r.sawyerReq.Patch()
|
||||||
case sawyer.DeleteMethod:
|
case sawyer.DeleteMethod:
|
||||||
sawyerResp = r.sawyerReq.Delete()
|
sawyerResp = r.sawyerReq.Delete()
|
||||||
|
|
2
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/users.go
сгенерированный
поставляемый
2
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/users.go
сгенерированный
поставляемый
|
@ -44,6 +44,7 @@ func (u *UsersService) All() (users []User, result *Result) {
|
||||||
type User struct {
|
type User struct {
|
||||||
*hypermedia.HALResource
|
*hypermedia.HALResource
|
||||||
|
|
||||||
|
SiteAdmin bool `json:"site_admin,omitempty"`
|
||||||
Login string `json:"login,omitempty"`
|
Login string `json:"login,omitempty"`
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
AvatarURL string `json:"avatar_url,omitempty"`
|
AvatarURL string `json:"avatar_url,omitempty"`
|
||||||
|
@ -57,7 +58,6 @@ type User struct {
|
||||||
Hireable bool `json:"hireable,omitempty"`
|
Hireable bool `json:"hireable,omitempty"`
|
||||||
Bio string `json:"bio,omitempty"`
|
Bio string `json:"bio,omitempty"`
|
||||||
PublicRepos int `json:"public_repos,omitempty"`
|
PublicRepos int `json:"public_repos,omitempty"`
|
||||||
PublicGists int `json:"public_gists,omitempty"`
|
|
||||||
Followers int `json:"followers,omitempty"`
|
Followers int `json:"followers,omitempty"`
|
||||||
Following int `json:"following,omitempty"`
|
Following int `json:"following,omitempty"`
|
||||||
HTMLURL string `json:"html_url,omitempty"`
|
HTMLURL string `json:"html_url,omitempty"`
|
||||||
|
|
8
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/users_test.go
сгенерированный
поставляемый
8
Godeps/_workspace/src/github.com/jingweno/go-octokit/octokit/users_test.go
сгенерированный
поставляемый
|
@ -25,10 +25,10 @@ func TestUsersService_GetCurrentUser(t *testing.T) {
|
||||||
assert.Equal(t, "jingweno", user.Login)
|
assert.Equal(t, "jingweno", user.Login)
|
||||||
assert.Equal(t, "jingweno@gmail.com", user.Email)
|
assert.Equal(t, "jingweno@gmail.com", user.Email)
|
||||||
assert.Equal(t, "User", user.Type)
|
assert.Equal(t, "User", user.Type)
|
||||||
assert.Equal(t, 3, user.Following)
|
assert.Equal(t, 17, user.Following)
|
||||||
assert.Equal(t, 19, user.Followers)
|
assert.Equal(t, 28, user.Followers)
|
||||||
assert.Equal(t, 80, user.PublicRepos)
|
assert.Equal(t, 90, user.PublicRepos)
|
||||||
assert.Equal(t, 25, user.PublicGists)
|
assert.Equal(t, false, user.SiteAdmin)
|
||||||
assert.Equal(t, "https://api.github.com/users/jingweno/repos", string(user.ReposURL))
|
assert.Equal(t, "https://api.github.com/users/jingweno/repos", string(user.ReposURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,7 @@ func All() []*Command {
|
||||||
all = append(all, cmdAlias)
|
all = append(all, cmdAlias)
|
||||||
all = append(all, cmdVersion)
|
all = append(all, cmdVersion)
|
||||||
all = append(all, cmdHelp)
|
all = append(all, cmdHelp)
|
||||||
|
all = append(all, cmdUpdate)
|
||||||
|
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ GitHub Commands:
|
||||||
issue Manipulate issues (beta)
|
issue Manipulate issues (beta)
|
||||||
|
|
||||||
See 'git help <command>' for more information on a specific command.
|
See 'git help <command>' for more information on a specific command.
|
||||||
|
Run 'git update' to update to the latest version of gh.
|
||||||
`
|
`
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
updater "github.com/inconshreveable/go-update"
|
||||||
|
"github.com/jingweno/gh/github"
|
||||||
|
"github.com/jingweno/gh/utils"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdUpdate = &Command{
|
||||||
|
Run: update,
|
||||||
|
Usage: "update",
|
||||||
|
Short: "Update gh",
|
||||||
|
Long: `Update gh to the latest version.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
git update
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(cmd *Command, args *Args) {
|
||||||
|
err := doUpdate()
|
||||||
|
utils.Check(err)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doUpdate() (err error) {
|
||||||
|
client := github.NewClient(github.GitHubHost)
|
||||||
|
releases, err := client.Releases(github.NewProject("jingweno", "gh", github.GitHubHost))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error fetching releases: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
latestRelease := releases[0]
|
||||||
|
tagName := latestRelease.TagName
|
||||||
|
version := strings.TrimPrefix(tagName, "v")
|
||||||
|
if version == Version {
|
||||||
|
err = fmt.Errorf("You're already on the latest version: %s", Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updating gh to release %s...\n", version)
|
||||||
|
downloadURL := fmt.Sprintf("https://github.com/jingweno/gh/releases/download/%s/gh_%s-snapshot_%s_%s.zip", tagName, version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
path, err := downloadFile(downloadURL)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can't download update from %s to %s", downloadURL, path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec, err := unzipExecutable(path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can't unzip gh executable: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err, _ = updater.FromFile(exec)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println("Done!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func unzipExecutable(path string) (exec string, err error) {
|
||||||
|
rc, err := zip.OpenReader(path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can't open zip file %s: %s", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
for _, file := range rc.File {
|
||||||
|
if !strings.HasPrefix(file.Name, "gh") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
exec, err = unzipFile(file, dir)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if exec == "" && err == nil {
|
||||||
|
err = fmt.Errorf("No gh executable is found in %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func unzipFile(file *zip.File, to string) (exec string, err error) {
|
||||||
|
frc, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can't open zip entry %s when reading: %s", file.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer frc.Close()
|
||||||
|
|
||||||
|
dest := filepath.Join(to, filepath.Base(file.Name))
|
||||||
|
f, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
copied, err := io.Copy(f, frc)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(copied) != file.UncompressedSize {
|
||||||
|
err = fmt.Errorf("Zip entry %s is corrupted", file.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec = f.Name()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadFile(url string) (path string, err error) {
|
||||||
|
dir, err := ioutil.TempDir("", "gh-update")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(filepath.Join(dir, filepath.Base(url)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path = file.Name()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"ConfigVersion": "0.9",
|
||||||
|
"BuildConstraints": "linux",
|
||||||
|
"TaskSettings": {
|
||||||
|
"archive-tar-gz": {
|
||||||
|
"platforms": "!linux"
|
||||||
|
},
|
||||||
|
"archive-zip": {
|
||||||
|
"platforms": "linux"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче