diff options
author | Michael Stapelberg <stapelberg@debian.org> | 2013-03-04 21:27:36 +0100 |
---|---|---|
committer | Michael Stapelberg <michael@stapelberg.de> | 2013-03-04 21:27:36 +0100 |
commit | 04b08da9af0c450d645ab7389d1467308cfc2db8 (patch) | |
tree | db247935fa4f2f94408edc3acd5d0d4f997aa0d8 /misc | |
parent | 917c5fb8ec48e22459d77e3849e6d388f93d3260 (diff) | |
download | golang-04b08da9af0c450d645ab7389d1467308cfc2db8.tar.gz |
Imported Upstream version 1.1~hg20130304upstream/1.1_hg20130304
Diffstat (limited to 'misc')
105 files changed, 3528 insertions, 1601 deletions
diff --git a/misc/benchcmp b/misc/benchcmp index 015e7d2b2..3180f57ea 100755 --- a/misc/benchcmp +++ b/misc/benchcmp @@ -7,8 +7,13 @@ case "$1" in -*) echo 'usage: benchcmp old.txt new.txt' >&2 echo >&2 - echo 'Each input file should be go test -test.run=NONE -test.bench=. > [old,new].txt' >&2 + echo 'Each input file should be from:' >&2 + echo ' go test -test.run=NONE -test.bench=. > [old,new].txt' >&2 + echo >&2 echo 'Benchcmp compares the first and last for each benchmark.' >&2 + echo >&2 + echo 'If -test.benchmem=true is added to the "go test" command' >&2 + echo 'benchcmp will also compare memory allocations.' >&2 exit 2 esac @@ -27,10 +32,31 @@ $1 ~ /Benchmark/ && $4 == "ns/op" { new[$1] = $3 if($6 == "MB/s") newmb[$1] = $5 + + # allocs/op might be at $8 or $10 depending on if + # SetBytes was used or not. + # B/op might be at $6 or $8, it should be immediately + # followed by allocs/op + if($8 == "allocs/op") { + newbytes[$1] = $5 + newalloc[$1] = $7 + } + if($10 == "allocs/op") { + newbytes[$1] = $7 + newalloc[$1] = $9 + } } else { old[$1] = $3 - if($6 = "MB/s") + if($6 == "MB/s") oldmb[$1] = $5 + if($8 == "allocs/op") { + oldbytes[$1] = $5 + oldalloc[$1] = $7 + } + if($10 == "allocs/op") { + oldbytes[$1] = $7 + oldalloc[$1] = $9 + } } } @@ -62,5 +88,37 @@ END { sprintf("%.2f", newmb[what]), sprintf("%.2f", newmb[what]/oldmb[what])) } + + # print allocs + anyalloc = 0 + for(i=0; i<n; i++) { + what = name[i] + if(!(what in newalloc)) + continue + if(anyalloc++ == 0) + printf("\n%-*s %12s %12s %7s\n", len, "benchmark", "old allocs", "new allocs", "delta") + if(oldalloc[what] == 0) + delta="n/a" + else + delta=sprintf("%.2f", 100*newalloc[what]/oldalloc[what]-100) + printf("%-*s %12d %12d %6s%%\n", len, what, + oldalloc[what], newalloc[what], delta) + } + + # print alloc bytes + anybytes = 0 + for(i=0; i<n; i++) { + what = name[i] + if(!(what in newbytes)) + continue + if(anybytes++ == 0) + printf("\n%-*s %12s %12s %7s\n", len, "benchmark", "old bytes", "new bytes", "delta") + if(oldbytes[what] == 0) + delta="n/a" + else + delta=sprintf("%.2f", 100*newbytes[what]/oldbytes[what]-100) + printf("%-*s %12d %12d %6s%%\n", len, what, + oldbytes[what], newbytes[what], delta) + } } ' "$@" diff --git a/misc/cgo/life/life.go b/misc/cgo/life/life.go index ec000ce3a..fda5495e5 100644 --- a/misc/cgo/life/life.go +++ b/misc/cgo/life/life.go @@ -1,3 +1,5 @@ +// skip + // Copyright 2010 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. @@ -9,8 +11,8 @@ import "C" import "unsafe" -func Run(gen, x, y int, a []int) { - n := make([]int, x*y) +func Run(gen, x, y int, a []int32) { + n := make([]int32, x*y) for i := 0; i < gen; i++ { C.Step(C.int(x), C.int(y), (*C.int)(unsafe.Pointer(&a[0])), (*C.int)(unsafe.Pointer(&n[0]))) copy(a, n) diff --git a/misc/cgo/life/main.go b/misc/cgo/life/main.go index 47ae0e18c..725e10f76 100644 --- a/misc/cgo/life/main.go +++ b/misc/cgo/life/main.go @@ -1,3 +1,5 @@ +// cmpout + // Copyright 2010 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. @@ -22,7 +24,7 @@ var gen = flag.Int("gen", 10, "generations") func main() { flag.Parse() - var a [MAXDIM * MAXDIM]int + var a [MAXDIM * MAXDIM]int32 for i := 2; i < *dim; i += 8 { for j := 2; j < *dim-3; j += 8 { for y := 0; y < 3; y++ { diff --git a/misc/cgo/life/golden.out b/misc/cgo/life/main.out index 539d2106d..26fc9c6e3 100644 --- a/misc/cgo/life/golden.out +++ b/misc/cgo/life/main.out @@ -1,4 +1,3 @@ -* life XXX XXX diff --git a/misc/cgo/life/test.bash b/misc/cgo/life/test.bash deleted file mode 100755 index bb483522c..000000000 --- a/misc/cgo/life/test.bash +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# Copyright 2010 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. - -set -e -go build -o life main.go - -echo '*' life >run.out -./life >>run.out -diff run.out golden.out - -rm -f life - diff --git a/misc/cgo/stdio/chain.go b/misc/cgo/stdio/chain.go index 1cf0b1fe5..a55cefa40 100644 --- a/misc/cgo/stdio/chain.go +++ b/misc/cgo/stdio/chain.go @@ -1,3 +1,5 @@ +// cmpout + // Copyright 2009 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. diff --git a/misc/cgo/stdio/chain.out b/misc/cgo/stdio/chain.out new file mode 100644 index 000000000..963cf9b66 --- /dev/null +++ b/misc/cgo/stdio/chain.out @@ -0,0 +1,55 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 diff --git a/misc/cgo/stdio/fib.go b/misc/cgo/stdio/fib.go index 6d3ccfd52..981ffeb9a 100644 --- a/misc/cgo/stdio/fib.go +++ b/misc/cgo/stdio/fib.go @@ -1,3 +1,5 @@ +// cmpout + // Copyright 2009 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. diff --git a/misc/cgo/stdio/fib.out b/misc/cgo/stdio/fib.out new file mode 100644 index 000000000..17ff50335 --- /dev/null +++ b/misc/cgo/stdio/fib.out @@ -0,0 +1,91 @@ +0 +1 +1 +2 +3 +5 +8 +13 +21 +34 +55 +89 +144 +233 +377 +610 +987 +1597 +2584 +4181 +6765 +10946 +17711 +28657 +46368 +75025 +121393 +196418 +317811 +514229 +832040 +1346269 +2178309 +3524578 +5702887 +9227465 +14930352 +24157817 +39088169 +63245986 +102334155 +165580141 +267914296 +433494437 +701408733 +1134903170 +1836311903 +2971215073 +4807526976 +7778742049 +12586269025 +20365011074 +32951280099 +53316291173 +86267571272 +139583862445 +225851433717 +365435296162 +591286729879 +956722026041 +1548008755920 +2504730781961 +4052739537881 +6557470319842 +10610209857723 +17167680177565 +27777890035288 +44945570212853 +72723460248141 +117669030460994 +190392490709135 +308061521170129 +498454011879264 +806515533049393 +1304969544928657 +2111485077978050 +3416454622906707 +5527939700884757 +8944394323791464 +14472334024676221 +23416728348467685 +37889062373143906 +61305790721611591 +99194853094755497 +160500643816367088 +259695496911122585 +420196140727489673 +679891637638612258 +1100087778366101931 +1779979416004714189 +2880067194370816120 diff --git a/misc/cgo/stdio/file.go b/misc/cgo/stdio/file.go index 6e7d479ad..e7bb906a5 100644 --- a/misc/cgo/stdio/file.go +++ b/misc/cgo/stdio/file.go @@ -1,3 +1,5 @@ +// skip + // Copyright 2009 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. @@ -23,9 +25,6 @@ import "unsafe" type File C.FILE -var Stdout = (*File)(C.stdout) -var Stderr = (*File)(C.stderr) - // Test reference to library symbol. // Stdout and stderr are too special to be a reliable test. //var = C.environ diff --git a/misc/cgo/stdio/hello.go b/misc/cgo/stdio/hello.go index 4ab3c7447..9cfeefbba 100644 --- a/misc/cgo/stdio/hello.go +++ b/misc/cgo/stdio/hello.go @@ -1,3 +1,5 @@ +// cmpout + // Copyright 2009 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. diff --git a/misc/cgo/stdio/hello.out b/misc/cgo/stdio/hello.out new file mode 100644 index 000000000..4b5fa6370 --- /dev/null +++ b/misc/cgo/stdio/hello.out @@ -0,0 +1 @@ +hello, world diff --git a/misc/cgo/stdio/golden.out b/misc/cgo/stdio/run.out index c0e496547..c0e496547 100644 --- a/misc/cgo/stdio/golden.out +++ b/misc/cgo/stdio/run.out diff --git a/misc/cgo/stdio/stdio.go b/misc/cgo/stdio/stdio.go new file mode 100644 index 000000000..76cb8ad80 --- /dev/null +++ b/misc/cgo/stdio/stdio.go @@ -0,0 +1,22 @@ +// skip + +// Copyright 2009 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 stdio + +/* +#include <stdio.h> + +// on mingw, stderr and stdout are defined as &_iob[FILENO] +// on netbsd, they are defined as &__sF[FILENO] +// and cgo doesn't recognize them, so write a function to get them, +// instead of depending on internals of libc implementation. +FILE *getStdout(void) { return stdout; } +FILE *getStderr(void) { return stderr; } +*/ +import "C" + +var Stdout = (*File)(C.getStdout()) +var Stderr = (*File)(C.getStderr()) diff --git a/misc/cgo/stdio/test.bash b/misc/cgo/stdio/test.bash deleted file mode 100755 index 21829fa31..000000000 --- a/misc/cgo/stdio/test.bash +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# Copyright 2009 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. - -set -e -go build hello.go -go build fib.go -go build chain.go - -echo '*' hello >run.out -./hello >>run.out -echo '*' fib >>run.out -./fib >>run.out -echo '*' chain >>run.out -./chain >>run.out -diff run.out golden.out - -rm -f hello fib chain - diff --git a/misc/cgo/test/Makefile b/misc/cgo/test/Makefile deleted file mode 100644 index 2b7187acb..000000000 --- a/misc/cgo/test/Makefile +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2011 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. - -include ../../../src/Make.inc - -TARG=runtime/cgotest - -CGOFILES=\ - align.go\ - basic.go\ - callback.go\ - env.go\ - exports.go\ - helpers.go\ - issue1222.go\ - issue1328.go\ - issue1560.go\ - issue2462.go\ - duplicate_symbol.go\ - -CGO_OFILES=\ - callback_c.o\ - -OFILES=\ - runtime.$O\ - -ifeq ($(GOOS),windows) -GCCVERSION=$(shell gcc -dumpversion) -ifeq ($(GOARCH),386) -GCCLIBDIR=/mingw/lib/gcc/mingw32/$(GCCVERSION) -CHKSTK=_chkstk.o -else -GCCLIBDIR=/mingw/lib/gcc/x86_64-w64-mingw32/$(GCCVERSION) -CHKSTK=_chkstk_ms.o -endif - -CGOFILES+=sleep_windows.go -CGO_OFILES+=$(CHKSTK) - -$(CHKSTK): - ar -x "$(GCCLIBDIR)/libgcc.a" $@ -endif - -include ../../../src/Make.pkg - -test: - echo cgo: tests disabled. gotest is gone. TODO
\ No newline at end of file diff --git a/misc/cgo/test/backdoor/runtime.c b/misc/cgo/test/backdoor/runtime.c index 54e6a1ef8..194a9c8e4 100644 --- a/misc/cgo/test/backdoor/runtime.c +++ b/misc/cgo/test/backdoor/runtime.c @@ -6,6 +6,8 @@ // Must be in a non-cgo-using package so that // the go command compiles this file with 6c, not gcc. +// +build gc + typedef char bool; bool runtime·lockedOSThread(void); diff --git a/misc/cgo/test/backdoor/runtime_gccgo.c b/misc/cgo/test/backdoor/runtime_gccgo.c new file mode 100644 index 000000000..218b2c3eb --- /dev/null +++ b/misc/cgo/test/backdoor/runtime_gccgo.c @@ -0,0 +1,18 @@ +// 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. + +// Expose some runtime functions for testing. +// This is the gccgo version of runtime.c. + +// +build gccgo + +_Bool runtime_lockedOSThread(void); + +_Bool LockedOSThread(void) asm(GOPKGPATH ".LockedOSThread"); + +_Bool +LockedOSThread(void) +{ + return runtime_lockedOSThread(); +} diff --git a/misc/cgo/test/basic.go b/misc/cgo/test/basic.go index 70ec5e43a..79cbf2b9c 100644 --- a/misc/cgo/test/basic.go +++ b/misc/cgo/test/basic.go @@ -14,15 +14,16 @@ package cgotest #define SHIFT(x, y) ((x)<<(y)) #define KILO SHIFT(1, 10) +#define UINT32VAL 0xc008427bU enum E { Enum1 = 1, Enum2 = 2, }; -typedef unsigned char uuid_t[20]; +typedef unsigned char cgo_uuid_t[20]; -void uuid_generate(uuid_t x) { +void uuid_generate(cgo_uuid_t x) { x[0] = 0; } @@ -55,6 +56,7 @@ int add(int x, int y) { */ import "C" import ( + "runtime" "syscall" "testing" "unsafe" @@ -65,7 +67,7 @@ const EINVAL = C.EINVAL /* test #define */ var KILO = C.KILO func uuidgen() { - var uuid C.uuid_t + var uuid C.cgo_uuid_t C.uuid_generate(&uuid[0]) } @@ -118,7 +120,12 @@ func testErrno(t *testing.T) { func testMultipleAssign(t *testing.T) { p := C.CString("234") n, m := C.strtol(p, nil, 345), C.strtol(p, nil, 10) - if n != 0 || m != 234 { + if runtime.GOOS == "openbsd" { + // Bug in OpenBSD strtol(3) - base > 36 succeeds. + if (n != 0 && n != 239089) || m != 234 { + t.Fatal("Strtol x2: ", n, m) + } + } else if n != 0 || m != 234 { t.Fatal("Strtol x2: ", n, m) } C.free(unsafe.Pointer(p)) @@ -141,3 +148,12 @@ func benchCgoCall(b *testing.B) { C.add(x, y) } } + +// Issue 2470. +func testUnsignedInt(t *testing.T) { + a := (int64)(C.UINT32VAL) + b := (int64)(0xc008427b) + if a != b { + t.Errorf("Incorrect unsigned int - got %x, want %x", a, b) + } +} diff --git a/misc/cgo/test/callback.go b/misc/cgo/test/callback.go index e6a1462b3..4f5d3f855 100644 --- a/misc/cgo/test/callback.go +++ b/misc/cgo/test/callback.go @@ -110,7 +110,7 @@ func testZeroArgCallback(t *testing.T) { func goFoo() { x := 1 for i := 0; i < 10000; i++ { - // variadic call mallocs + writes to + // variadic call mallocs + writes to variadic(x, x, x) if x != 1 { panic("bad x") diff --git a/misc/cgo/test/cgo_linux_test.go b/misc/cgo/test/cgo_linux_test.go new file mode 100644 index 000000000..056d67c96 --- /dev/null +++ b/misc/cgo/test/cgo_linux_test.go @@ -0,0 +1,9 @@ +// 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 cgotest + +import "testing" + +func TestSetgid(t *testing.T) { testSetgid(t) } diff --git a/misc/cgo/test/cgo_test.go b/misc/cgo/test/cgo_test.go index 34beee69d..536fa507a 100644 --- a/misc/cgo/test/cgo_test.go +++ b/misc/cgo/test/cgo_test.go @@ -6,7 +6,7 @@ package cgotest import "testing" -// The actual test functions are in non-_test.go files +// The actual test functions are in non-_test.go files // so that they can use cgo (import "C"). // These wrappers are here for gotest to find. @@ -16,6 +16,7 @@ func TestEnum(t *testing.T) { testEnum(t) } func TestAtol(t *testing.T) { testAtol(t) } func TestErrno(t *testing.T) { testErrno(t) } func TestMultipleAssign(t *testing.T) { testMultipleAssign(t) } +func TestUnsignedInt(t *testing.T) { testUnsignedInt(t) } func TestCallback(t *testing.T) { testCallback(t) } func TestCallbackGC(t *testing.T) { testCallbackGC(t) } func TestCallbackPanic(t *testing.T) { testCallbackPanic(t) } @@ -27,5 +28,13 @@ func Test1328(t *testing.T) { test1328(t) } func TestParallelSleep(t *testing.T) { testParallelSleep(t) } func TestSetEnv(t *testing.T) { testSetEnv(t) } func TestHelpers(t *testing.T) { testHelpers(t) } +func TestLibgcc(t *testing.T) { testLibgcc(t) } +func Test1635(t *testing.T) { test1635(t) } +func TestPrintf(t *testing.T) { testPrintf(t) } +func Test4029(t *testing.T) { test4029(t) } +func TestBoolAlign(t *testing.T) { testBoolAlign(t) } +func Test3729(t *testing.T) { test3729(t) } +func Test3775(t *testing.T) { test3775(t) } +func TestCthread(t *testing.T) { testCthread(t) } func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) } diff --git a/misc/cgo/test/cthread.go b/misc/cgo/test/cthread.go new file mode 100644 index 000000000..d918d033f --- /dev/null +++ b/misc/cgo/test/cthread.go @@ -0,0 +1,44 @@ +// Copyright 2013 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 cgotest + +// extern void doAdd(int, int); +import "C" + +import ( + "runtime" + "sync" + "testing" +) + +var sum struct { + sync.Mutex + i int +} + +//export Add +func Add(x int) { + defer func() { + recover() + }() + sum.Lock() + sum.i += x + sum.Unlock() + var p *int + *p = 2 +} + +func testCthread(t *testing.T) { + if runtime.GOARCH == "arm" { + t.Skip("testCthread disabled on arm") + } + + C.doAdd(10, 6) + + want := 10 * (10 - 1) / 2 * 6 + if sum.i != want { + t.Fatalf("sum=%d, want %d", sum.i, want) + } +} diff --git a/misc/cgo/test/cthread_unix.c b/misc/cgo/test/cthread_unix.c new file mode 100644 index 000000000..998bc00cb --- /dev/null +++ b/misc/cgo/test/cthread_unix.c @@ -0,0 +1,34 @@ +// Copyright 2013 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 linux netbsd openbsd + +#include <pthread.h> +#include "_cgo_export.h" + +static void* +addThread(void *p) +{ + int i, max; + + max = *(int*)p; + for(i=0; i<max; i++) + Add(i); + return 0; +} + +void +doAdd(int max, int nthread) +{ + enum { MaxThread = 20 }; + int i; + pthread_t thread_id[MaxThread]; + + if(nthread > MaxThread) + nthread = MaxThread; + for(i=0; i<nthread; i++) + pthread_create(&thread_id[i], 0, addThread, &max); + for(i=0; i<nthread; i++) + pthread_join(thread_id[i], 0); +} diff --git a/misc/cgo/test/cthread_windows.c b/misc/cgo/test/cthread_windows.c new file mode 100644 index 000000000..5f370a818 --- /dev/null +++ b/misc/cgo/test/cthread_windows.c @@ -0,0 +1,37 @@ +// Copyright 2013 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. + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <process.h> +#include "_cgo_export.h" + +__stdcall +static unsigned int +addThread(void *p) +{ + int i, max; + + max = *(int*)p; + for(i=0; i<max; i++) + Add(i); + return 0; +} + +void +doAdd(int max, int nthread) +{ + enum { MaxThread = 20 }; + int i; + uintptr_t thread_id[MaxThread]; + + if(nthread > MaxThread) + nthread = MaxThread; + for(i=0; i<nthread; i++) + thread_id[i] = _beginthreadex(0, 0, addThread, &max, 0, 0); + for(i=0; i<nthread; i++) { + WaitForSingleObject((HANDLE)thread_id[i], INFINITE); + CloseHandle((HANDLE)thread_id[i]); + } +} diff --git a/misc/cgo/test/issue1560.go b/misc/cgo/test/issue1560.go index 3faa966e7..147ce94b5 100644 --- a/misc/cgo/test/issue1560.go +++ b/misc/cgo/test/issue1560.go @@ -15,6 +15,7 @@ void twoSleep(int); import "C" import ( + "runtime" "testing" "time" ) @@ -27,19 +28,48 @@ func parallelSleep(n int) { } //export BackgroundSleep -func BackgroundSleep(n int) { +func BackgroundSleep(n int32) { go func() { C.sleep(C.uint(n)) sleepDone <- true }() } +// wasteCPU starts a background goroutine to waste CPU +// to cause the power management to raise the CPU frequency. +// On ARM this has the side effect of making sleep more accurate. +func wasteCPU() chan struct{} { + done := make(chan struct{}) + go func() { + for { + select { + case <-done: + return + default: + } + } + }() + // pause for a short amount of time to allow the + // power management to recognise load has risen. + <-time.After(300 * time.Millisecond) + return done +} + func testParallelSleep(t *testing.T) { + if runtime.GOARCH == "arm" { + // on ARM, the 1.3s deadline is frequently missed, + // and burning cpu seems to help + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(2)) + defer close(wasteCPU()) + } + + sleepSec := 1 start := time.Now() - parallelSleep(1) - dt := time.Now().Sub(start) - // bug used to run sleeps in serial, producing a 2-second delay. - if dt >= 1300*time.Millisecond { - t.Fatalf("parallel 1-second sleeps slept for %f seconds", dt.Seconds()) + parallelSleep(sleepSec) + dt := time.Since(start) + t.Logf("sleep(%d) slept for %v", sleepSec, dt) + // bug used to run sleeps in serial, producing a 2*sleepSec-second delay. + if dt >= time.Duration(sleepSec)*1300*time.Millisecond { + t.Fatalf("parallel %d-second sleeps slept for %f seconds", sleepSec, dt.Seconds()) } } diff --git a/misc/cgo/test/issue1635.go b/misc/cgo/test/issue1635.go new file mode 100644 index 000000000..6bfe110fd --- /dev/null +++ b/misc/cgo/test/issue1635.go @@ -0,0 +1,33 @@ +// 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 cgotest + +/* +// Mac OS X's gcc will generate scattered relocation 2/1 for +// this function on Darwin/386, and 8l couldn't handle it. +// this example is in issue 1635 +#include <stdio.h> +void scatter() { + void *p = scatter; + printf("scatter = %p\n", p); +} + +// this example is in issue 3253 +int hola = 0; +int testHola() { return hola; } +*/ +import "C" + +import "testing" + +func test1635(t *testing.T) { + C.scatter() + if v := C.hola; v != 0 { + t.Fatalf("C.hola is %d, should be 0", v) + } + if v := C.testHola(); v != 0 { + t.Fatalf("C.testHola() is %d, should be 0", v) + } +} diff --git a/misc/cgo/test/issue3261.go b/misc/cgo/test/issue3261.go new file mode 100644 index 000000000..0411be892 --- /dev/null +++ b/misc/cgo/test/issue3261.go @@ -0,0 +1,43 @@ +// 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 cgotest + +/* +// libgcc on ARM might be compiled as thumb code, but our 5l +// can't handle that, so we have to disable this test on arm. +#ifdef __ARMEL__ +#include <stdio.h> +int vabs(int x) { + puts("testLibgcc is disabled on ARM because 5l cannot handle thumb library."); + return (x < 0) ? -x : x; +} +#else +int __absvsi2(int); // dummy prototype for libgcc function +// we shouldn't name the function abs, as gcc might use +// the builtin one. +int vabs(int x) { return __absvsi2(x); } +#endif +*/ +import "C" + +import "testing" + +func testLibgcc(t *testing.T) { + var table = []struct { + in, out C.int + }{ + {0, 0}, + {1, 1}, + {-42, 42}, + {1000300, 1000300}, + {1 - 1<<31, 1<<31 - 1}, + } + for _, v := range table { + if o := C.vabs(v.in); o != v.out { + t.Fatalf("abs(%d) got %d, should be %d", v.in, o, v.out) + return + } + } +} diff --git a/misc/cgo/test/issue3729.go b/misc/cgo/test/issue3729.go new file mode 100644 index 000000000..1bea38b6e --- /dev/null +++ b/misc/cgo/test/issue3729.go @@ -0,0 +1,47 @@ +// 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. + +// Issue 3729: cmd/cgo: access errno from void C function +// void f(void) returns [0]byte, error in Go world. + +// +build !windows + +package cgotest + +/* +#include <errno.h> + +void g(void) { + errno = E2BIG; +} + +// try to pass some non-trivial arguments to function g2 +const char _expA = 0x42; +const float _expB = 3.14159; +const short _expC = 0x55aa; +const int _expD = 0xdeadbeef; +void g2(int x, char a, float b, short c, int d) { + if (a == _expA && b == _expB && c == _expC && d == _expD) + errno = x; + else + errno = -1; +} +*/ +import "C" + +import ( + "syscall" + "testing" +) + +func test3729(t *testing.T) { + _, e := C.g() + if e != syscall.E2BIG { + t.Errorf("got %q, expect %q", e, syscall.E2BIG) + } + _, e = C.g2(C.EINVAL, C._expA, C._expB, C._expC, C._expD) + if e != syscall.EINVAL { + t.Errorf("got %q, expect %q", e, syscall.EINVAL) + } +} diff --git a/misc/cgo/test/issue3729w.go b/misc/cgo/test/issue3729w.go new file mode 100644 index 000000000..702115b81 --- /dev/null +++ b/misc/cgo/test/issue3729w.go @@ -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. + +// Issue 3729: cmd/cgo: access errno from void C function +// void f(void) returns [0]byte, error in Go world. + +// +build windows + +package cgotest + +import "testing" + +func test3729(t *testing.T) { + t.Log("skip errno test on Windows") +} diff --git a/misc/cgo/test/issue3741.go b/misc/cgo/test/issue3741.go new file mode 100644 index 000000000..3d3bbf951 --- /dev/null +++ b/misc/cgo/test/issue3741.go @@ -0,0 +1,22 @@ +// 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 cgotest + +import "C" + +//export exportSliceIn +func exportSliceIn(s []byte) bool { + return len(s) == cap(s) +} + +//export exportSliceOut +func exportSliceOut() []byte { + return []byte{1} +} + +//export exportSliceInOut +func exportSliceInOut(s []byte) []byte { + return s +} diff --git a/misc/cgo/test/issue3775.go b/misc/cgo/test/issue3775.go new file mode 100644 index 000000000..c05a5d4be --- /dev/null +++ b/misc/cgo/test/issue3775.go @@ -0,0 +1,29 @@ +package cgotest + +/* +void lockOSThreadCallback(void); +inline static void lockOSThreadC(void) +{ + lockOSThreadCallback(); +} +int usleep(unsigned usec); +*/ +import "C" + +import ( + "runtime" + "testing" +) + +func test3775(t *testing.T) { + // Used to panic because of the UnlockOSThread below. + C.lockOSThreadC() +} + +//export lockOSThreadCallback +func lockOSThreadCallback() { + runtime.LockOSThread() + runtime.UnlockOSThread() + go C.usleep(10000) + runtime.Gosched() +} diff --git a/misc/cgo/test/issue3945.go b/misc/cgo/test/issue3945.go new file mode 100644 index 000000000..331cd0baf --- /dev/null +++ b/misc/cgo/test/issue3945.go @@ -0,0 +1,22 @@ +// 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 cgotest + +// Test that cgo reserves enough stack space during cgo call. +// See http://golang.org/issue/3945 for details. + +// #include <stdio.h> +// +// void say() { +// printf("%s from C\n", "hello"); +// } +// +import "C" + +import "testing" + +func testPrintf(t *testing.T) { + C.say() +} diff --git a/misc/cgo/test/issue4029.go b/misc/cgo/test/issue4029.go new file mode 100644 index 000000000..7495d38fe --- /dev/null +++ b/misc/cgo/test/issue4029.go @@ -0,0 +1,60 @@ +// 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 !windows + +package cgotest + +/* +#include <dlfcn.h> +#cgo linux LDFLAGS: -ldl +*/ +import "C" + +import ( + "fmt" + "testing" +) + +//export IMPIsOpaque +func IMPIsOpaque() { + fmt.Println("isOpaque") +} + +//export IMPInitWithFrame +func IMPInitWithFrame() { + fmt.Println("IInitWithFrame") +} + +//export IMPDrawRect +func IMPDrawRect() { + fmt.Println("drawRect:") +} + +//export IMPWindowResize +func IMPWindowResize() { + fmt.Println("windowDidResize:") +} + +func test4029(t *testing.T) { + loadThySelf(t, "IMPWindowResize") + loadThySelf(t, "IMPDrawRect") + loadThySelf(t, "IMPInitWithFrame") + loadThySelf(t, "IMPIsOpaque") +} + +func loadThySelf(t *testing.T, symbol string) { + this_process := C.dlopen(nil, C.RTLD_NOW) + if this_process == nil { + t.Fatal("dlopen:", C.GoString(C.dlerror())) + } + defer C.dlclose(this_process) + + symbol_address := C.dlsym(this_process, C.CString(symbol)) + if symbol_address == nil { + t.Fatal("dlsym:", C.GoString(C.dlerror())) + } else { + t.Log(symbol, symbol_address) + } +} diff --git a/misc/cgo/test/issue4029w.go b/misc/cgo/test/issue4029w.go new file mode 100644 index 000000000..1cf43df37 --- /dev/null +++ b/misc/cgo/test/issue4029w.go @@ -0,0 +1,12 @@ +// 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 windows + +package cgotest + +import "testing" + +func test4029(t *testing.T) { +} diff --git a/misc/cgo/test/issue4054a.go b/misc/cgo/test/issue4054a.go new file mode 100644 index 000000000..2abdac590 --- /dev/null +++ b/misc/cgo/test/issue4054a.go @@ -0,0 +1,23 @@ +// 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 cgotest + +/* +typedef enum { + A = 0, + B, + C, + D, + E, + F, + G, + H, + I, + J, +} issue4054a; +*/ +import "C" + +var issue4054a = []int{C.A, C.B, C.C, C.D, C.E, C.F, C.G, C.H, C.I, C.J} diff --git a/misc/cgo/test/issue4054b.go b/misc/cgo/test/issue4054b.go new file mode 100644 index 000000000..048964c89 --- /dev/null +++ b/misc/cgo/test/issue4054b.go @@ -0,0 +1,23 @@ +// 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 cgotest + +/* +typedef enum { + A = 0, + B, + C, + D, + E, + F, + G, + H, + I, + J, +} issue4054b; +*/ +import "C" + +var issue4054b = []int{C.A, C.B, C.C, C.D, C.E, C.F, C.G, C.H, C.I, C.J} diff --git a/misc/cgo/test/issue4273.c b/misc/cgo/test/issue4273.c new file mode 100644 index 000000000..a3fcf3b0a --- /dev/null +++ b/misc/cgo/test/issue4273.c @@ -0,0 +1,10 @@ +// 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. + +#ifdef __ELF__ +__attribute__((weak)) +__attribute__((visibility("hidden"))) +void _compilerrt_abort_impl(const char *file, int line, const char *func) { +} +#endif diff --git a/misc/cgo/test/issue4273b.c b/misc/cgo/test/issue4273b.c new file mode 100644 index 000000000..93e2f4fab --- /dev/null +++ b/misc/cgo/test/issue4273b.c @@ -0,0 +1,11 @@ +// 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. + +#ifdef __ELF__ +extern void _compilerrt_abort_impl(const char *file, int line, const char *func); + +void __my_abort(const char *file, int line, const char *func) { + _compilerrt_abort_impl(file, line, func); +} +#endif diff --git a/misc/cgo/test/issue4417.go b/misc/cgo/test/issue4417.go new file mode 100644 index 000000000..0b48071d4 --- /dev/null +++ b/misc/cgo/test/issue4417.go @@ -0,0 +1,42 @@ +// 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. + +// Issue 4417: cmd/cgo: bool alignment/padding issue. +// bool alignment is wrong and causing wrong arguments when calling functions. +// + +package cgotest + +/* +#include <stdbool.h> + +static int c_bool(bool a, bool b, int c, bool d, bool e) { + return c; +} +*/ +import "C" +import "testing" + +func testBoolAlign(t *testing.T) { + b := C.c_bool(true, true, 10, true, false) + if b != 10 { + t.Fatalf("found %d expected 10\n", b) + } + b = C.c_bool(true, true, 5, true, true) + if b != 5 { + t.Fatalf("found %d expected 5\n", b) + } + b = C.c_bool(true, true, 3, true, false) + if b != 3 { + t.Fatalf("found %d expected 3\n", b) + } + b = C.c_bool(false, false, 1, true, false) + if b != 1 { + t.Fatalf("found %d expected 1\n", b) + } + b = C.c_bool(false, true, 200, true, false) + if b != 200 { + t.Fatalf("found %d expected 200\n", b) + } +} diff --git a/misc/cgo/test/setgid_linux.go b/misc/cgo/test/setgid_linux.go new file mode 100644 index 000000000..829afce1b --- /dev/null +++ b/misc/cgo/test/setgid_linux.go @@ -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. + +// Test that setgid does not hang on GNU/Linux. +// See http://code.google.com/p/go/issues/detail?id=3871 for details. + +package cgotest + +/* +#include <sys/types.h> +#include <unistd.h> +*/ +import "C" + +import ( + "testing" + "time" +) + +func testSetgid(t *testing.T) { + c := make(chan bool) + go func() { + C.setgid(0) + c <- true + }() + select { + case <-c: + case <-time.After(5 * time.Second): + t.Error("setgid hung") + } +} diff --git a/misc/cgo/test/sleep_windows.go b/misc/cgo/test/sleep_windows_386.go index 007a1bb4c..75687d783 100644 --- a/misc/cgo/test/sleep_windows.go +++ b/misc/cgo/test/sleep_windows_386.go @@ -5,6 +5,10 @@ package cgotest /* +// mingw32 on windows/386 provides usleep() but not sleep(), +// as we don't want to require all other OSes to provide usleep, +// we emulate sleep(int s) using win32 API Sleep(int ms). + #include <windows.h> unsigned int sleep(unsigned int seconds) { diff --git a/misc/cgo/testso/test.bash b/misc/cgo/testso/test.bash index ecef873c8..5f113d216 100755 --- a/misc/cgo/testso/test.bash +++ b/misc/cgo/testso/test.bash @@ -4,7 +4,7 @@ # license that can be found in the LICENSE file. set -e -gcc $(go env GOGCCFLAGS) -shared -o libcgosotest.so cgoso_c.c +$(go env CC) $(go env GOGCCFLAGS) -shared -o libcgosotest.so cgoso_c.c go build main.go LD_LIBRARY_PATH=. ./main rm -f libcgosotest.so main diff --git a/misc/chrome/gophertool/background.html b/misc/chrome/gophertool/background.html index 058c18142..06daa98b1 100644 --- a/misc/chrome/gophertool/background.html +++ b/misc/chrome/gophertool/background.html @@ -6,19 +6,7 @@ --> <head> <script src="gopher.js"></script> -<script> - -chrome.omnibox.onInputEntered.addListener(function(t) { - var url = urlForInput(t); - if (url) { - chrome.tabs.getSelected(null, function(tab) { - if (!tab) return; - chrome.tabs.update(tab.id, { "url": url, "selected": true }); - }); - } -}); - -</script> +<script src="background.js"></script> </head> </html> diff --git a/misc/chrome/gophertool/background.js b/misc/chrome/gophertool/background.js new file mode 100644 index 000000000..d18faa986 --- /dev/null +++ b/misc/chrome/gophertool/background.js @@ -0,0 +1,9 @@ +chrome.omnibox.onInputEntered.addListener(function(t) { + var url = urlForInput(t); + if (url) { + chrome.tabs.getSelected(null, function(tab) { + if (!tab) return; + chrome.tabs.update(tab.id, { "url": url, "selected": true }); + }); + } +}); diff --git a/misc/chrome/gophertool/gopher.js b/misc/chrome/gophertool/gopher.js index 847c1c70d..3238f0fcc 100644 --- a/misc/chrome/gophertool/gopher.js +++ b/misc/chrome/gophertool/gopher.js @@ -12,7 +12,7 @@ function urlForInput(t) { } if (numericRE.test(t)) { - if (t < 1000000) { + if (t < 150000) { return "http://code.google.com/p/go/issues/detail?id=" + t; } return "http://codereview.appspot.com/" + t + "/"; diff --git a/misc/chrome/gophertool/manifest.json b/misc/chrome/gophertool/manifest.json index 3a2540a86..04386594a 100644 --- a/misc/chrome/gophertool/manifest.json +++ b/misc/chrome/gophertool/manifest.json @@ -1,11 +1,14 @@ { "name": "Hacking Gopher", "version": "1.0", + "manifest_version": 2, "description": "Go Hacking utility", - "background_page": "background.html", + "background": { + "page": "background.html" + }, "browser_action": { "default_icon": "gopher.png", - "popup": "popup.html" + "default_popup": "popup.html" }, "omnibox": { "keyword": "golang" }, "icons": { diff --git a/misc/chrome/gophertool/popup.html b/misc/chrome/gophertool/popup.html index 4816c392b..8bb7795fa 100644 --- a/misc/chrome/gophertool/popup.html +++ b/misc/chrome/gophertool/popup.html @@ -6,49 +6,14 @@ --> <head> <script src="gopher.js"></script> -<script> - -function focusinput() { - document.getElementById("inputbox").focus(); -} - -function navigate() { - var box = document.getElementById("inputbox"); - box.focus(); - - var t = box.value; - if (t == "") { - return false; - } - - var success = function(url) { - console.log("matched " + t + " to: " + url) - box.value = ""; - openURL(url); - return false; // cancel form submission - }; - - var url = urlForInput(t); - if (url) { - return success(url); - } - - console.log("no match for text: " + t) - return false; -} - -function openURL(url) { - chrome.tabs.create({ "url": url }) -} - -</script> +<script src="popup.js"></script> </head> -<body onload="focusinput()" style='margin: 0.5em; font-family: sans;'> -<small><a href="#" onclick="openURL('http://code.google.com/p/go/issues/list')">issue</a>, -<a href="#" onclick="openURL('http://codereview.appspot.com/')">codereview</a>, -<a href="#" onclick="openURL('http://code.google.com/p/go/source/list')">commit</a>, or -<a href="#" onclick="openURL('http://golang.org/pkg/')">pkg</a> id/name:</small> -<form style='margin: 0' onsubmit="return navigate();"><nobr><input id="inputbox" size=10 /><input type="submit" value="go" /></nobr></form> -<small>Also: <a href="#" onclick="openURL('http://build.golang.org/')">buildbots</small> +<body style='margin: 0.5em; font-family: sans;'> +<small><a href="#" url="http://code.google.com/p/go/issues/list">issue</a>, +<a href="#" url="http://codereview.appspot.com/">codereview</a>, +<a href="#" url="http://code.google.com/p/go/source/list">commit</a>, or +<a href="#" url="http://golang.org/pkg/">pkg</a> id/name:</small> +<form style='margin: 0' id='navform'><nobr><input id="inputbox" size=10 tabindex=1 /><input type="submit" value="go" /></nobr></form> +<small>Also: <a href="#" url="http://build.golang.org">buildbots</a></small> </body> </html> diff --git a/misc/chrome/gophertool/popup.js b/misc/chrome/gophertool/popup.js new file mode 100644 index 000000000..410d65120 --- /dev/null +++ b/misc/chrome/gophertool/popup.js @@ -0,0 +1,46 @@ +function openURL(url) { + chrome.tabs.create({ "url": url }) +} + +function addLinks() { + var links = document.getElementsByTagName("a"); + for (var i = 0; i < links.length; i++) { + var url = links[i].getAttribute("url"); + if (url) + links[i].addEventListener("click", function () { + openURL(this.getAttribute("url")); + }); + } +} + +window.addEventListener("load", function () { + addLinks(); + console.log("hacking gopher pop-up loaded."); + document.getElementById("inputbox").focus(); +}); + +window.addEventListener("submit", function () { + console.log("submitting form"); + var box = document.getElementById("inputbox"); + box.focus(); + + var t = box.value; + if (t == "") { + return false; + } + + var success = function(url) { + console.log("matched " + t + " to: " + url) + box.value = ""; + openURL(url); + return false; // cancel form submission + }; + + var url = urlForInput(t); + if (url) { + return success(url); + } + + console.log("no match for text: " + t) + return false; +}); diff --git a/misc/dashboard/app/app.yaml b/misc/dashboard/app/app.yaml index 6e19db09c..c5a1f6cb8 100644 --- a/misc/dashboard/app/app.yaml +++ b/misc/dashboard/app/app.yaml @@ -6,7 +6,7 @@ application: golang-org version: build runtime: go -api_version: go1beta +api_version: go1 handlers: - url: /static diff --git a/misc/dashboard/app/build/build.go b/misc/dashboard/app/build/build.go index c49fa8bb2..e0c0f0048 100644 --- a/misc/dashboard/app/build/build.go +++ b/misc/dashboard/app/build/build.go @@ -49,6 +49,10 @@ func (p *Package) LastCommit(c appengine.Context) (*Commit, error) { Order("-Time"). Limit(1). GetAll(c, &commits) + if _, ok := err.(*datastore.ErrFieldMismatch); ok { + // Some fields have been removed, so it's okay to ignore this error. + err = nil + } if err != nil { return nil, err } @@ -65,6 +69,10 @@ func GetPackage(c appengine.Context, path string) (*Package, error) { if err == datastore.ErrNoSuchEntity { return nil, fmt.Errorf("package %q not found", path) } + if _, ok := err.(*datastore.ErrFieldMismatch); ok { + // Some fields have been removed, so it's okay to ignore this error. + err = nil + } return p, err } @@ -111,19 +119,35 @@ func (c *Commit) Valid() error { return nil } +// each result line is approx 105 bytes. This constant is a tradeoff between +// build history and the AppEngine datastore limit of 1mb. +const maxResults = 1000 + // AddResult adds the denormalized Reuslt data to the Commit's Result field. // It must be called from inside a datastore transaction. func (com *Commit) AddResult(c appengine.Context, r *Result) error { if err := datastore.Get(c, com.Key(c), com); err != nil { return fmt.Errorf("getting Commit: %v", err) } - com.ResultData = append(com.ResultData, r.Data()) + com.ResultData = trim(append(com.ResultData, r.Data()), maxResults) if _, err := datastore.Put(c, com.Key(c), com); err != nil { return fmt.Errorf("putting Commit: %v", err) } return nil } +func trim(s []string, n int) []string { + l := min(len(s), n) + return s[len(s)-l:] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + // Result returns the build Result for this Commit for the given builder/goHash. func (c *Commit) Result(builder, goHash string) *Result { for _, r := range c.ResultData { @@ -160,20 +184,11 @@ func partsToHash(c *Commit, p []string) *Result { } } -// OK returns the Commit's build state for a specific builder and goHash. -func (c *Commit) OK(builder, goHash string) (ok, present bool) { - r := c.Result(builder, goHash) - if r == nil { - return false, false - } - return r.OK, true -} - // A Result describes a build result for a Commit on an OS/architecture. // // Each Result entity is a descendant of its associated Commit entity. type Result struct { - Builder string // "arch-os[-note]" + Builder string // "os-arch[-note]" Hash string PackagePath string // (empty for Go commits) @@ -184,7 +199,7 @@ type Result struct { Log string `datastore:"-"` // for JSON unmarshaling only LogHash string `datastore:",noindex"` // Key to the Log record. - RunTime int64 // time to build+test in nanoseconds + RunTime int64 // time to build+test in nanoseconds } func (r *Result) Key(c appengine.Context) *datastore.Key { @@ -297,7 +312,12 @@ func Packages(c appengine.Context, kind string) ([]*Package, error) { q := datastore.NewQuery("Package").Filter("Kind=", kind) for t := q.Run(c); ; { pkg := new(Package) - if _, err := t.Next(pkg); err == datastore.Done { + _, err := t.Next(pkg) + if _, ok := err.(*datastore.ErrFieldMismatch); ok { + // Some fields have been removed, so it's okay to ignore this error. + err = nil + } + if err == datastore.Done { break } else if err != nil { return nil, err diff --git a/misc/dashboard/app/build/handler.go b/misc/dashboard/app/build/handler.go index 5d1e3094c..1a1118641 100644 --- a/misc/dashboard/app/build/handler.go +++ b/misc/dashboard/app/build/handler.go @@ -322,7 +322,7 @@ func resultHandler(r *http.Request) (interface{}, error) { // logHandler displays log text for a given hash. // It handles paths like "/log/hash". func logHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-type", "text/plain") + w.Header().Set("Content-type", "text/plain; charset=utf-8") c := appengine.NewContext(r) hash := r.URL.Path[len("/log/"):] key := datastore.NewKey(c, "Log", hash, 0, nil) diff --git a/misc/dashboard/app/build/init.go b/misc/dashboard/app/build/init.go index 5311688b7..85a766b9d 100644 --- a/misc/dashboard/app/build/init.go +++ b/misc/dashboard/app/build/init.go @@ -24,6 +24,8 @@ var subRepos = []string{ "crypto", "image", "net", + "talks", + "exp", } // Put subRepos into defaultPackages. @@ -42,7 +44,12 @@ func initHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) defer cache.Tick(c) for _, p := range defaultPackages { - if err := datastore.Get(c, p.Key(c), new(Package)); err == nil { + err := datastore.Get(c, p.Key(c), new(Package)) + if _, ok := err.(*datastore.ErrFieldMismatch); ok { + // Some fields have been removed, so it's okay to ignore this error. + err = nil + } + if err == nil { continue } else if err != datastore.ErrNoSuchEntity { logErr(w, r, err) diff --git a/misc/dashboard/app/build/notify.go b/misc/dashboard/app/build/notify.go index e02344ca8..52b91f6c1 100644 --- a/misc/dashboard/app/build/notify.go +++ b/misc/dashboard/app/build/notify.go @@ -21,6 +21,13 @@ const ( domain = "build.golang.org" ) +// failIgnore is a set of builders that we don't email about because +// they're too flaky. +var failIgnore = map[string]bool{ + "netbsd-386-bsiegert": true, + "netbsd-amd64-bsiegert": true, +} + // notifyOnFailure checks whether the supplied Commit or the subsequent // Commit (if present) breaks the build for this builder. // If either of those commits break the build an email notification is sent @@ -30,6 +37,10 @@ const ( // This must be run in a datastore transaction, and the provided *Commit must // have been retrieved from the datastore within that transaction. func notifyOnFailure(c appengine.Context, com *Commit, builder string) error { + if failIgnore[builder] { + return nil + } + // TODO(adg): implement notifications for packages if com.PackagePath != "" { return nil @@ -37,15 +48,15 @@ func notifyOnFailure(c appengine.Context, com *Commit, builder string) error { p := &Package{Path: com.PackagePath} var broken *Commit - ok, present := com.OK(builder, "") - if !present { + cr := com.Result(builder, "") + if cr == nil { return fmt.Errorf("no result for %s/%s", com.Hash, builder) } q := datastore.NewQuery("Commit").Ancestor(p.Key(c)) - if ok { + if cr.OK { // This commit is OK. Notify if next Commit is broken. next := new(Commit) - q.Filter("ParentHash=", com.Hash) + q = q.Filter("ParentHash=", com.Hash) if err := firstMatch(c, q, next); err != nil { if err == datastore.ErrNoSuchEntity { // OK at tip, no notification necessary. @@ -53,13 +64,15 @@ func notifyOnFailure(c appengine.Context, com *Commit, builder string) error { } return err } - if ok, present := next.OK(builder, ""); present && !ok { + if nr := next.Result(builder, ""); nr != nil && !nr.OK { + c.Debugf("commit ok: %#v\nresult: %#v", com, cr) + c.Debugf("next commit broken: %#v\nnext result:%#v", next, nr) broken = next } } else { // This commit is broken. Notify if the previous Commit is OK. prev := new(Commit) - q.Filter("Hash=", com.ParentHash) + q = q.Filter("Hash=", com.ParentHash) if err := firstMatch(c, q, prev); err != nil { if err == datastore.ErrNoSuchEntity { // No previous result, let the backfill of @@ -68,7 +81,9 @@ func notifyOnFailure(c appengine.Context, com *Commit, builder string) error { } return err } - if ok, present := prev.OK(builder, ""); present && ok { + if pr := prev.Result(builder, ""); pr != nil && pr.OK { + c.Debugf("commit broken: %#v\nresult: %#v", com, cr) + c.Debugf("previous commit ok: %#v\nprevious result:%#v", prev, pr) broken = com } } diff --git a/misc/dashboard/app/build/test.go b/misc/dashboard/app/build/test.go index d8470fec1..7e5539236 100644 --- a/misc/dashboard/app/build/test.go +++ b/misc/dashboard/app/build/test.go @@ -43,14 +43,15 @@ var testPackages = []*Package{ var tCommitTime = time.Now().Add(-time.Hour * 24 * 7) -func tCommit(hash, parentHash string) *Commit { +func tCommit(hash, parentHash, path string) *Commit { tCommitTime.Add(time.Hour) // each commit should have a different time return &Commit{ - Hash: hash, - ParentHash: parentHash, - Time: tCommitTime, - User: "adg", - Desc: "change description", + PackagePath: path, + Hash: hash, + ParentHash: parentHash, + Time: tCommitTime, + User: "adg", + Desc: "change description " + hash, } } @@ -64,9 +65,9 @@ var testRequests = []struct { {"/packages?kind=subrepo", nil, nil, []*Package{testPackage}}, // Go repo - {"/commit", nil, tCommit("0001", "0000"), nil}, - {"/commit", nil, tCommit("0002", "0001"), nil}, - {"/commit", nil, tCommit("0003", "0002"), nil}, + {"/commit", nil, tCommit("0001", "0000", ""), nil}, + {"/commit", nil, tCommit("0002", "0001", ""), nil}, + {"/commit", nil, tCommit("0003", "0002", ""), nil}, {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}}, {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-amd64"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}}, {"/result", nil, &Result{Builder: "linux-386", Hash: "0001", OK: true}, nil}, @@ -81,12 +82,12 @@ var testRequests = []struct { {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-amd64"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0002"}}}, // branches - {"/commit", nil, tCommit("0004", "0003"), nil}, - {"/commit", nil, tCommit("0005", "0002"), nil}, + {"/commit", nil, tCommit("0004", "0003", ""), nil}, + {"/commit", nil, tCommit("0005", "0002", ""), nil}, {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0005"}}}, {"/result", nil, &Result{Builder: "linux-386", Hash: "0005", OK: true}, nil}, {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0004"}}}, - {"/result", nil, &Result{Builder: "linux-386", Hash: "0004", OK: true}, nil}, + {"/result", nil, &Result{Builder: "linux-386", Hash: "0004", OK: false}, nil}, {"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}}, // logs @@ -98,9 +99,9 @@ var testRequests = []struct { {"/result", nil, &Result{Builder: "linux-386", Hash: "0003", OK: false, Log: "test"}, nil}, // non-Go repos - {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1001", ParentHash: "1000"}, nil}, - {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1002", ParentHash: "1001"}, nil}, - {"/commit", nil, &Commit{PackagePath: testPkg, Hash: "1003", ParentHash: "1002"}, nil}, + {"/commit", nil, tCommit("1001", "1000", testPkg), nil}, + {"/commit", nil, tCommit("1002", "1001", testPkg), nil}, + {"/commit", nil, tCommit("1003", "1002", testPkg), nil}, {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1003"}}}, {"/result", nil, &Result{PackagePath: testPkg, Builder: "linux-386", Hash: "1003", GoHash: "0001", OK: true}, nil}, {"/todo", url.Values{"kind": {"build-package"}, "builder": {"linux-386"}, "packagePath": {testPkg}, "goHash": {"0001"}}, nil, &Todo{Kind: "build-package", Data: &Commit{Hash: "1002"}}}, @@ -230,7 +231,7 @@ func testHandler(w http.ResponseWriter, r *http.Request) { return } } - fmt.Fprint(w, "PASS") + fmt.Fprint(w, "PASS\nYou should see only one mail notification (for 0003/linux-386) in the dev_appserver logs.") } func nukeEntities(c appengine.Context, kinds []string) error { diff --git a/misc/dashboard/app/build/ui.go b/misc/dashboard/app/build/ui.go index 0337aa306..cc3629a5a 100644 --- a/misc/dashboard/app/build/ui.go +++ b/misc/dashboard/app/build/ui.go @@ -97,7 +97,7 @@ type Pagination struct { func goCommits(c appengine.Context, page int) ([]*Commit, error) { q := datastore.NewQuery("Commit"). Ancestor((&Package{}).Key(c)). - Order("-Time"). + Order("-Num"). Limit(commitsPerPage). Offset(page * commitsPerPage) var commits []*Commit @@ -211,6 +211,9 @@ func builderArch(s string) string { // builderArchShort returns a short arch tag for a builder string func builderArchShort(s string) string { + if s == "linux-amd64-race" { + return "race" + } arch := builderArch(s) switch arch { case "amd64": diff --git a/misc/dashboard/builder/Makefile b/misc/dashboard/builder/Makefile index abf3755ab..4e4d408bf 100644 --- a/misc/dashboard/builder/Makefile +++ b/misc/dashboard/builder/Makefile @@ -2,8 +2,8 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -gobuilder: $(shell ls *.go) +builder: $(shell ls *.go) go build -o $@ $^ clean: - rm -f gobuilder + rm -f builder diff --git a/misc/dashboard/builder/doc.go b/misc/dashboard/builder/doc.go index 30d8fe948..519286170 100644 --- a/misc/dashboard/builder/doc.go +++ b/misc/dashboard/builder/doc.go @@ -4,15 +4,15 @@ /* -Go Builder is a continuous build client for the Go project. +Go Builder is a continuous build client for the Go project. It integrates with the Go Dashboard AppEngine application. Go Builder is intended to run continuously as a background process. -It periodically pulls updates from the Go Mercurial repository. +It periodically pulls updates from the Go Mercurial repository. When a newer revision is found, Go Builder creates a clone of the repository, -runs all.bash, and reports build success or failure to the Go Dashboard. +runs all.bash, and reports build success or failure to the Go Dashboard. For a release revision (a change description that matches "release.YYYY-MM-DD"), Go Builder will create a tar.gz archive of the GOROOT and deliver it to the @@ -22,7 +22,7 @@ Usage: gobuilder goos-goarch... - Several goos-goarch combinations can be provided, and the builder will + Several goos-goarch combinations can be provided, and the builder will build them in serial. Optional flags: @@ -55,4 +55,4 @@ If the Google Code credentials are not provided the archival step will be skipped. */ -package documentation +package main diff --git a/misc/dashboard/builder/exec.go b/misc/dashboard/builder/exec.go index 802d5f079..a4aabd284 100644 --- a/misc/dashboard/builder/exec.go +++ b/misc/dashboard/builder/exec.go @@ -6,14 +6,16 @@ package main import ( "bytes" + "fmt" "io" "log" "os" "os/exec" + "time" ) // run is a simple wrapper for exec.Run/Close -func run(envv []string, dir string, argv ...string) error { +func run(timeout time.Duration, envv []string, dir string, argv ...string) error { if *verbose { log.Println("run", argv) } @@ -21,43 +23,57 @@ func run(envv []string, dir string, argv ...string) error { cmd.Dir = dir cmd.Env = envv cmd.Stderr = os.Stderr - return cmd.Run() + if err := cmd.Start(); err != nil { + return err + } + return waitWithTimeout(timeout, cmd) } -// runLog runs a process and returns the combined stdout/stderr, -// as well as writing it to logfile (if specified). It returns -// process combined stdout and stderr output, exit status and error. -// The error returned is nil, if process is started successfully, -// even if exit status is not successful. -func runLog(envv []string, logfile, dir string, argv ...string) (string, int, error) { - if *verbose { - log.Println("runLog", argv) - } +// runLog runs a process and returns the combined stdout/stderr. It returns +// process combined stdout and stderr output, exit status and error. The +// error returned is nil, if process is started successfully, even if exit +// status is not successful. +func runLog(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error) { + var b bytes.Buffer + ok, err := runOutput(timeout, envv, &b, dir, argv...) + return b.String(), ok, err +} - b := new(bytes.Buffer) - var w io.Writer = b - if logfile != "" { - f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return "", 0, err - } - defer f.Close() - w = io.MultiWriter(f, b) +// runOutput runs a process and directs any output to the supplied writer. +// It returns exit status and error. The error returned is nil, if process +// is started successfully, even if exit status is not successful. +func runOutput(timeout time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error) { + if *verbose { + log.Println("runOutput", argv) } cmd := exec.Command(argv[0], argv[1:]...) cmd.Dir = dir cmd.Env = envv - cmd.Stdout = w - cmd.Stderr = w + cmd.Stdout = out + cmd.Stderr = out startErr := cmd.Start() if startErr != nil { - return "", 1, startErr + return false, startErr + } + if err := waitWithTimeout(timeout, cmd); err != nil { + return false, err } - exitStatus := 0 - if err := cmd.Wait(); err != nil { - exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead. + return true, nil +} + +func waitWithTimeout(timeout time.Duration, cmd *exec.Cmd) error { + errc := make(chan error, 1) + go func() { + errc <- cmd.Wait() + }() + var err error + select { + case <-time.After(timeout): + cmd.Process.Kill() + err = fmt.Errorf("timed out after %v", timeout) + case err = <-errc: } - return b.String(), exitStatus, nil + return err } diff --git a/misc/dashboard/builder/http.go b/misc/dashboard/builder/http.go index e50ae5724..b50e84551 100644 --- a/misc/dashboard/builder/http.go +++ b/misc/dashboard/builder/http.go @@ -56,8 +56,10 @@ func dash(meth, cmd string, args url.Values, req, resp interface{}) error { if err != nil { return err } - defer r.Body.Close() + if r.StatusCode != http.StatusOK { + return fmt.Errorf("bad http response: %v", r.Status) + } body := new(bytes.Buffer) if _, err := body.ReadFrom(r.Body); err != nil { return err diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go index 85bb7ad4b..b2b8f43a6 100644 --- a/misc/dashboard/builder/main.go +++ b/misc/dashboard/builder/main.go @@ -6,16 +6,15 @@ package main import ( "bytes" - "encoding/xml" "flag" "fmt" + "io" "io/ioutil" "log" "os" "path/filepath" "regexp" "runtime" - "strconv" "strings" "time" ) @@ -32,6 +31,7 @@ const ( // These variables are copied from the gobuilder's environment // to the envv of its subprocesses. var extraEnv = []string{ + "CC", "GOARM", "GOHOSTARCH", "GOHOSTOS", @@ -40,25 +40,27 @@ var extraEnv = []string{ } type Builder struct { + goroot *Repo name string goos, goarch string key string } var ( - buildroot = flag.String("buildroot", filepath.Join(os.TempDir(), "gobuilder"), "Directory under which to build") - commitFlag = flag.Bool("commit", false, "upload information about new commits") - dashboard = flag.String("dashboard", "build.golang.org", "Go Dashboard Host") - buildRelease = flag.Bool("release", false, "Build and upload binary release archives") - buildRevision = flag.String("rev", "", "Build specified revision and exit") - buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)") - failAll = flag.Bool("fail", false, "fail all builds") - parallel = flag.Bool("parallel", false, "Build multiple targets in parallel") - verbose = flag.Bool("v", false, "verbose") + buildroot = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build") + dashboard = flag.String("dashboard", "build.golang.org", "Go Dashboard Host") + buildRelease = flag.Bool("release", false, "Build and upload binary release archives") + buildRevision = flag.String("rev", "", "Build specified revision and exit") + buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)") + failAll = flag.Bool("fail", false, "fail all builds") + parallel = flag.Bool("parallel", false, "Build multiple targets in parallel") + buildTimeout = flag.Duration("buildTimeout", 60*time.Minute, "Maximum time to wait for builds and tests") + cmdTimeout = flag.Duration("cmdTimeout", 5*time.Minute, "Maximum time to wait for an external command") + commitInterval = flag.Duration("commitInterval", 1*time.Minute, "Time to wait between polling for new commits (0 disables commit poller)") + verbose = flag.Bool("v", false, "verbose") ) var ( - goroot string binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) allCmd = "all" + suffix @@ -73,26 +75,15 @@ func main() { os.Exit(2) } flag.Parse() - if len(flag.Args()) == 0 && !*commitFlag { + if len(flag.Args()) == 0 { flag.Usage() } - goroot = filepath.Join(*buildroot, "goroot") - builders := make([]*Builder, len(flag.Args())) - for i, builder := range flag.Args() { - b, err := NewBuilder(builder) - if err != nil { - log.Fatal(err) - } - builders[i] = b - } - - if *failAll { - failMode(builders) - return + goroot := &Repo{ + Path: filepath.Join(*buildroot, "goroot"), } // set up work environment, use existing enviroment if possible - if hgRepoExists(goroot) { + if goroot.Exists() || *failAll { log.Print("Found old workspace, will use it") } else { if err := os.RemoveAll(*buildroot); err != nil { @@ -101,22 +92,31 @@ func main() { if err := os.Mkdir(*buildroot, mkdirPerm); err != nil { log.Fatalf("Error making build root (%s): %s", *buildroot, err) } - if err := hgClone(hgUrl, goroot); err != nil { + var err error + goroot, err = RemoteRepo(hgUrl).Clone(goroot.Path, "tip") + if err != nil { log.Fatal("Error cloning repository:", err) } } - if *commitFlag { - if len(flag.Args()) == 0 { - commitWatcher() - return + // set up builders + builders := make([]*Builder, len(flag.Args())) + for i, name := range flag.Args() { + b, err := NewBuilder(goroot, name) + if err != nil { + log.Fatal(err) } - go commitWatcher() + builders[i] = b + } + + if *failAll { + failMode(builders) + return } // if specified, build revision and return if *buildRevision != "" { - hash, err := fullHash(goroot, *buildRevision) + hash, err := goroot.FullHash(*buildRevision) if err != nil { log.Fatal("Error finding revision: ", err) } @@ -128,7 +128,10 @@ func main() { return } - // go continuous build mode (default) + // Start commit watcher + go commitWatcher(goroot) + + // go continuous build mode // check for new commits and build them for { built := false @@ -175,15 +178,18 @@ func failMode(builders []*Builder) { } } -func NewBuilder(builder string) (*Builder, error) { - b := &Builder{name: builder} +func NewBuilder(goroot *Repo, name string) (*Builder, error) { + b := &Builder{ + goroot: goroot, + name: name, + } // get goos/goarch from builder string - s := strings.SplitN(builder, "-", 3) + s := strings.SplitN(b.name, "-", 3) if len(s) >= 2 { b.goos, b.goarch = s[0], s[1] } else { - return nil, fmt.Errorf("unsupported builder form: %s", builder) + return nil, fmt.Errorf("unsupported builder form: %s", name) } // read keys from keyfile @@ -206,7 +212,7 @@ func NewBuilder(builder string) (*Builder, error) { } // build checks for a new commit for this builder -// and builds it if one is found. +// and builds it if one is found. // It returns true if a build was attempted. func (b *Builder) build() bool { hash, err := b.todo("build-go-commit", "", "") @@ -217,16 +223,8 @@ func (b *Builder) build() bool { if hash == "" { return false } - // Look for hash locally before running hg pull. - if _, err := fullHash(goroot, hash[:12]); err != nil { - // Don't have hash, so run hg pull. - if err := run(nil, goroot, "hg", "pull"); err != nil { - log.Println("hg pull failed:", err) - return false - } - } - err = b.buildHash(hash) - if err != nil { + + if err := b.buildHash(hash); err != nil { log.Println(err) } return true @@ -242,34 +240,49 @@ func (b *Builder) buildHash(hash string) error { } defer os.RemoveAll(workpath) - // clone repo - if err := run(nil, workpath, "hg", "clone", goroot, "go"); err != nil { + // pull before cloning to ensure we have the revision + if err := b.goroot.Pull(); err != nil { return err } - // update to specified revision - if err := run(nil, filepath.Join(workpath, "go"), "hg", "update", hash); err != nil { + // clone repo at specified revision + if _, err := b.goroot.Clone(filepath.Join(workpath, "go"), hash); err != nil { return err } srcDir := filepath.Join(workpath, "go", "src") // build + var buildlog bytes.Buffer logfile := filepath.Join(workpath, "build.log") + f, err := os.Create(logfile) + if err != nil { + return err + } + defer f.Close() + w := io.MultiWriter(f, &buildlog) + cmd := *buildCmd if !filepath.IsAbs(cmd) { cmd = filepath.Join(srcDir, cmd) } startTime := time.Now() - buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd) + ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd) runTime := time.Now().Sub(startTime) - if err != nil { - return fmt.Errorf("%s: %s", *buildCmd, err) + errf := func() string { + if err != nil { + return fmt.Sprintf("error: %v", err) + } + if !ok { + return "failed" + } + return "success" } + fmt.Fprintf(w, "Build complete, duration %v. Result: %v\n", runTime, errf()) - if status != 0 { + if err != nil || !ok { // record failure - return b.recordResult(false, "", hash, "", buildLog, runTime) + return b.recordResult(false, "", hash, "", buildlog.String(), runTime) } // record success @@ -278,13 +291,15 @@ func (b *Builder) buildHash(hash string) error { } // build Go sub-repositories - b.buildSubrepos(filepath.Join(workpath, "go"), hash) + goRoot := filepath.Join(workpath, "go") + goPath := workpath + b.buildSubrepos(goRoot, goPath, hash) return nil } // failBuild checks for a new commit for this builder -// and fails it if one is found. +// and fails it if one is found. // It returns true if a build was "attempted". func (b *Builder) failBuild() bool { hash, err := b.todo("build-go-commit", "", "") @@ -304,7 +319,7 @@ func (b *Builder) failBuild() bool { return true } -func (b *Builder) buildSubrepos(goRoot, goHash string) { +func (b *Builder) buildSubrepos(goRoot, goPath, goHash string) { for _, pkg := range dashboardPackages("subrepo") { // get the latest todo for this package hash, err := b.todo("build-package", pkg, goHash) @@ -320,7 +335,7 @@ func (b *Builder) buildSubrepos(goRoot, goHash string) { if *verbose { log.Printf("buildSubrepos %s: building %q", pkg, hash) } - buildLog, err := b.buildSubrepo(goRoot, pkg, hash) + buildLog, err := b.buildSubrepo(goRoot, goPath, pkg, hash) if err != nil { if buildLog == "" { buildLog = err.Error() @@ -338,45 +353,39 @@ func (b *Builder) buildSubrepos(goRoot, goHash string) { // buildSubrepo fetches the given package, updates it to the specified hash, // and runs 'go test -short pkg/...'. It returns the build log and any error. -func (b *Builder) buildSubrepo(goRoot, pkg, hash string) (string, error) { - goBin := filepath.Join(goRoot, "bin") - goTool := filepath.Join(goBin, "go") - env := append(b.envv(), "GOROOT="+goRoot) +func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error) { + goTool := filepath.Join(goRoot, "bin", "go") + env := append(b.envv(), "GOROOT="+goRoot, "GOPATH="+goPath) - // add goBin to PATH + // add $GOROOT/bin and $GOPATH/bin to PATH for i, e := range env { const p = "PATH=" if !strings.HasPrefix(e, p) { continue } - env[i] = p + goBin + string(os.PathListSeparator) + e[len(p):] + sep := string(os.PathListSeparator) + env[i] = p + filepath.Join(goRoot, "bin") + sep + filepath.Join(goPath, "bin") + sep + e[len(p):] } // fetch package and dependencies - log, status, err := runLog(env, "", goRoot, goTool, "get", "-d", pkg) - if err == nil && status != 0 { - err = fmt.Errorf("go exited with status %d", status) + log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...") + if err == nil && !ok { + err = fmt.Errorf("go exited with status 1") } if err != nil { - // 'go get -d' will fail for a subrepo because its top-level - // directory does not contain a go package. No matter, just - // check whether an hg directory exists and proceed. - hgDir := filepath.Join(goRoot, "src/pkg", pkg, ".hg") - if fi, e := os.Stat(hgDir); e != nil || !fi.IsDir() { - return log, err - } + return log, err } // hg update to the specified hash - pkgPath := filepath.Join(goRoot, "src/pkg", pkg) - if err := run(nil, pkgPath, "hg", "update", hash); err != nil { + repo := Repo{Path: filepath.Join(goPath, "src", pkg)} + if err := repo.UpdateTo(hash); err != nil { return "", err } // test the package - log, status, err = runLog(env, "", goRoot, goTool, "test", "-short", pkg+"/...") - if err == nil && status != 0 { - err = fmt.Errorf("go exited with status %d", status) + log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...") + if err == nil && !ok { + err = fmt.Errorf("go exited with status 1") } return log, err } @@ -449,9 +458,13 @@ func isFile(name string) bool { } // commitWatcher polls hg for new commits and tells the dashboard about them. -func commitWatcher() { +func commitWatcher(goroot *Repo) { + if *commitInterval == 0 { + log.Printf("commitInterval is %s, disabling commitWatcher", *commitInterval) + return + } // Create builder just to get master key. - b, err := NewBuilder("mercurial-commit") + b, err := NewBuilder(goroot, "mercurial-commit") if err != nil { log.Fatal(err) } @@ -462,104 +475,46 @@ func commitWatcher() { log.Printf("poll...") } // Main Go repository. - commitPoll(key, "") + commitPoll(goroot, "", key) // Go sub-repositories. for _, pkg := range dashboardPackages("subrepo") { - commitPoll(key, pkg) + pkgroot := &Repo{ + Path: filepath.Join(*buildroot, pkg), + } + commitPoll(pkgroot, pkg, key) } if *verbose { log.Printf("sleep...") } - time.Sleep(60e9) - } -} - -func hgClone(url, path string) error { - return run(nil, *buildroot, "hg", "clone", url, path) -} - -func hgRepoExists(path string) bool { - fi, err := os.Stat(filepath.Join(path, ".hg")) - if err != nil { - return false + time.Sleep(*commitInterval) } - return fi.IsDir() -} - -// HgLog represents a single Mercurial revision. -type HgLog struct { - Hash string - Author string - Date string - Desc string - Parent string - - // Internal metadata - added bool } // logByHash is a cache of all Mercurial revisions we know about, // indexed by full hash. var logByHash = map[string]*HgLog{} -// xmlLogTemplate is a template to pass to Mercurial to make -// hg log print the log in valid XML for parsing with xml.Unmarshal. -const xmlLogTemplate = ` - <Log> - <Hash>{node|escape}</Hash> - <Parent>{parent|escape}</Parent> - <Author>{author|escape}</Author> - <Date>{date|rfc3339date}</Date> - <Desc>{desc|escape}</Desc> - </Log> -` - // commitPoll pulls any new revisions from the hg server // and tells the server about them. -func commitPoll(key, pkg string) { - pkgRoot := goroot - - if pkg != "" { - pkgRoot = filepath.Join(*buildroot, pkg) - if !hgRepoExists(pkgRoot) { - if err := hgClone(repoURL(pkg), pkgRoot); err != nil { - log.Printf("%s: hg clone failed: %v", pkg, err) - if err := os.RemoveAll(pkgRoot); err != nil { - log.Printf("%s: %v", pkg, err) - } - return +func commitPoll(repo *Repo, pkg, key string) { + if !repo.Exists() { + var err error + repo, err = RemoteRepo(repoURL(pkg)).Clone(repo.Path, "tip") + if err != nil { + log.Printf("%s: hg clone failed: %v", pkg, err) + if err := os.RemoveAll(repo.Path); err != nil { + log.Printf("%s: %v", pkg, err) } } - } - - if err := run(nil, pkgRoot, "hg", "pull"); err != nil { - log.Printf("hg pull: %v", err) return } - const N = 50 // how many revisions to grab - - data, _, err := runLog(nil, "", pkgRoot, "hg", "log", - "--encoding=utf-8", - "--limit="+strconv.Itoa(N), - "--template="+xmlLogTemplate, - ) + logs, err := repo.Log() // repo.Log calls repo.Pull internally if err != nil { log.Printf("hg log: %v", err) return } - var logStruct struct { - Log []HgLog - } - err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct) - if err != nil { - log.Printf("unmarshal hg log: %v", err) - return - } - - logs := logStruct.Log - // Pass 1. Fill in parents and add new log entries to logsByHash. // Empty parent means take parent from next log entry. // Non-empty parent has form 1234:hashhashhash; we want full hash. @@ -568,7 +523,7 @@ func commitPoll(key, pkg string) { if l.Parent == "" && i+1 < len(logs) { l.Parent = logs[i+1].Hash } else if l.Parent != "" { - l.Parent, _ = fullHash(pkgRoot, l.Parent) + l.Parent, _ = repo.FullHash(l.Parent) } if *verbose { log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent) @@ -580,8 +535,7 @@ func commitPoll(key, pkg string) { } } - for i := range logs { - l := &logs[i] + for _, l := range logs { addCommit(pkg, l.Hash, key) } } @@ -625,28 +579,6 @@ func addCommit(pkg, hash, key string) bool { return true } -// fullHash returns the full hash for the given Mercurial revision. -func fullHash(root, rev string) (string, error) { - s, _, err := runLog(nil, "", root, - "hg", "log", - "--encoding=utf-8", - "--rev="+rev, - "--limit=1", - "--template={node}", - ) - if err != nil { - return "", nil - } - s = strings.TrimSpace(s) - if s == "" { - return "", fmt.Errorf("cannot find revision") - } - if len(s) != 40 { - return "", fmt.Errorf("hg returned invalid hash " + s) - } - return s, nil -} - var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`) // repoURL returns the repository URL for the supplied import path. @@ -668,6 +600,19 @@ func defaultSuffix() string { return ".bash" } +// defaultBuildRoot returns default buildroot directory. +func defaultBuildRoot() string { + var d string + if runtime.GOOS == "windows" { + // will use c:\, otherwise absolute paths become too long + // during builder run, see http://golang.org/issue/3358. + d = `c:\` + } else { + d = os.TempDir() + } + return filepath.Join(d, "gobuilder") +} + func getenvOk(k string) (v string, ok bool) { v = os.Getenv(k) if v != "" { diff --git a/misc/dashboard/builder/vcs.go b/misc/dashboard/builder/vcs.go new file mode 100644 index 000000000..63198a34b --- /dev/null +++ b/misc/dashboard/builder/vcs.go @@ -0,0 +1,148 @@ +// Copyright 2013 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 main + +import ( + "encoding/xml" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "sync" +) + +// Repo represents a mercurial repository. +type Repo struct { + Path string + sync.Mutex +} + +// RemoteRepo constructs a *Repo representing a remote repository. +func RemoteRepo(url string) *Repo { + return &Repo{ + Path: url, + } +} + +// Clone clones the current Repo to a new destination +// returning a new *Repo if successful. +func (r *Repo) Clone(path, rev string) (*Repo, error) { + r.Lock() + defer r.Unlock() + if err := run(*cmdTimeout, nil, *buildroot, r.hgCmd("clone", "-r", rev, r.Path, path)...); err != nil { + return nil, err + } + return &Repo{ + Path: path, + }, nil +} + +// UpdateTo updates the working copy of this Repo to the +// supplied revision. +func (r *Repo) UpdateTo(hash string) error { + r.Lock() + defer r.Unlock() + return run(*cmdTimeout, nil, r.Path, r.hgCmd("update", hash)...) +} + +// Exists reports whether this Repo represents a valid Mecurial repository. +func (r *Repo) Exists() bool { + fi, err := os.Stat(filepath.Join(r.Path, ".hg")) + if err != nil { + return false + } + return fi.IsDir() +} + +// Pull pulls changes from the default path, that is, the path +// this Repo was cloned from. +func (r *Repo) Pull() error { + r.Lock() + defer r.Unlock() + return run(*cmdTimeout, nil, r.Path, r.hgCmd("pull")...) +} + +// Log returns the changelog for this repository. +func (r *Repo) Log() ([]HgLog, error) { + if err := r.Pull(); err != nil { + return nil, err + } + const N = 50 // how many revisions to grab + + r.Lock() + defer r.Unlock() + data, _, err := runLog(*cmdTimeout, nil, r.Path, r.hgCmd("log", + "--encoding=utf-8", + "--limit="+strconv.Itoa(N), + "--template="+xmlLogTemplate)..., + ) + if err != nil { + return nil, err + } + + var logStruct struct { + Log []HgLog + } + err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct) + if err != nil { + log.Printf("unmarshal hg log: %v", err) + return nil, err + } + return logStruct.Log, nil +} + +// FullHash returns the full hash for the given Mercurial revision. +func (r *Repo) FullHash(rev string) (string, error) { + r.Lock() + defer r.Unlock() + s, _, err := runLog(*cmdTimeout, nil, r.Path, + r.hgCmd("log", + "--encoding=utf-8", + "--rev="+rev, + "--limit=1", + "--template={node}")..., + ) + if err != nil { + return "", nil + } + s = strings.TrimSpace(s) + if s == "" { + return "", fmt.Errorf("cannot find revision") + } + if len(s) != 40 { + return "", fmt.Errorf("hg returned invalid hash " + s) + } + return s, nil +} + +func (r *Repo) hgCmd(args ...string) []string { + return append([]string{"hg", "--config", "extensions.codereview=!"}, args...) +} + +// HgLog represents a single Mercurial revision. +type HgLog struct { + Hash string + Author string + Date string + Desc string + Parent string + + // Internal metadata + added bool +} + +// xmlLogTemplate is a template to pass to Mercurial to make +// hg log print the log in valid XML for parsing with xml.Unmarshal. +const xmlLogTemplate = ` + <Log> + <Hash>{node|escape}</Hash> + <Parent>{parent|escape}</Parent> + <Author>{author|escape}</Author> + <Date>{date|rfc3339date}</Date> + <Desc>{desc|escape}</Desc> + </Log> +` diff --git a/misc/dashboard/codereview/app.yaml b/misc/dashboard/codereview/app.yaml new file mode 100644 index 000000000..372eca5a1 --- /dev/null +++ b/misc/dashboard/codereview/app.yaml @@ -0,0 +1,24 @@ +application: gocodereview +version: 1 +runtime: go +api_version: go1 + +inbound_services: +- mail + +handlers: +- url: /static/(.*) + static_files: static/\1 + upload: static/.* +- url: /_ah/mail/.* + script: _go_app + login: admin +- url: /_ah/queue/go/delay + script: _go_app + login: admin +- url: /(gc|update-cl) + script: _go_app + login: admin +- url: /.* + script: _go_app + login: required diff --git a/misc/dashboard/codereview/cron.yaml b/misc/dashboard/codereview/cron.yaml new file mode 100644 index 000000000..3d33d32b5 --- /dev/null +++ b/misc/dashboard/codereview/cron.yaml @@ -0,0 +1,4 @@ +cron: +- description: GC + url: /gc + schedule: every 6 hours diff --git a/misc/dashboard/codereview/dashboard/cl.go b/misc/dashboard/codereview/dashboard/cl.go new file mode 100644 index 000000000..e150ea123 --- /dev/null +++ b/misc/dashboard/codereview/dashboard/cl.go @@ -0,0 +1,481 @@ +// 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 dashboard + +// This file handles operations on the CL entity kind. + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "io" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "sort" + "strings" + "time" + + "appengine" + "appengine/datastore" + "appengine/taskqueue" + "appengine/urlfetch" + "appengine/user" +) + +func init() { + http.HandleFunc("/assign", handleAssign) + http.HandleFunc("/update-cl", handleUpdateCL) +} + +const codereviewBase = "http://codereview.appspot.com" +const gobotBase = "http://research.swtch.com/gobot_codereview" + +var clRegexp = regexp.MustCompile(`\d+`) + +// CL represents a code review. +type CL struct { + Number string // e.g. "5903061" + Closed bool + Owner string // email address + + Created, Modified time.Time + + Description []byte `datastore:",noindex"` + FirstLine string `datastore:",noindex"` + LGTMs []string + NotLGTMs []string + LastUpdateBy string // author of most recent review message + LastUpdate string `datastore:",noindex"` // first line of most recent review message + + // Mail information. + Subject string `datastore:",noindex"` + Recipients []string `datastore:",noindex"` + LastMessageID string `datastore:",noindex"` + + // These are person IDs (e.g. "rsc"); they may be empty + Author string + Reviewer string +} + +// Reviewed reports whether the reviewer has replied to the CL. +// The heuristic is that the CL has been replied to if it is LGTMed +// or if the last CL message was from the reviewer. +func (cl *CL) Reviewed() bool { + if cl.LastUpdateBy == cl.Reviewer { + return true + } + if person := emailToPerson[cl.LastUpdateBy]; person != "" && person == cl.Reviewer { + return true + } + for _, who := range cl.LGTMs { + if who == cl.Reviewer { + return true + } + } + return false +} + +// DisplayOwner returns the CL's owner, either as their email address +// or the person ID if it's a reviewer. It is for display only. +func (cl *CL) DisplayOwner() string { + if p, ok := emailToPerson[cl.Owner]; ok { + return p + } + return cl.Owner +} + +func (cl *CL) FirstLineHTML() template.HTML { + s := template.HTMLEscapeString(cl.FirstLine) + // Embolden the package name. + if i := strings.Index(s, ":"); i >= 0 { + s = "<b>" + s[:i] + "</b>" + s[i:] + } + return template.HTML(s) +} + +func formatEmails(e []string) template.HTML { + x := make([]string, len(e)) + for i, s := range e { + s = template.HTMLEscapeString(s) + if !strings.Contains(s, "@") { + s = "<b>" + s + "</b>" + } + s = `<span class="email">` + s + "</span>" + x[i] = s + } + return template.HTML(strings.Join(x, ", ")) +} + +func (cl *CL) LGTMHTML() template.HTML { + return formatEmails(cl.LGTMs) +} + +func (cl *CL) NotLGTMHTML() template.HTML { + return formatEmails(cl.NotLGTMs) +} + +func (cl *CL) ModifiedAgo() string { + // Just the first non-zero unit. + units := [...]struct { + suffix string + unit time.Duration + }{ + {"d", 24 * time.Hour}, + {"h", time.Hour}, + {"m", time.Minute}, + {"s", time.Second}, + } + d := time.Now().Sub(cl.Modified) + for _, u := range units { + if d > u.unit { + return fmt.Sprintf("%d%s", d/u.unit, u.suffix) + } + } + return "just now" +} + +func handleAssign(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + + if r.Method != "POST" { + http.Error(w, "Bad method "+r.Method, 400) + return + } + + u := user.Current(c) + person, ok := emailToPerson[u.Email] + if !ok { + http.Error(w, "Not allowed", http.StatusUnauthorized) + return + } + + n, rev := r.FormValue("cl"), r.FormValue("r") + if !clRegexp.MatchString(n) { + c.Errorf("Bad CL %q", n) + http.Error(w, "Bad CL", 400) + return + } + if _, ok := preferredEmail[rev]; !ok && rev != "" { + c.Errorf("Unknown reviewer %q", rev) + http.Error(w, "Unknown reviewer", 400) + return + } + + key := datastore.NewKey(c, "CL", n, 0, nil) + + if rev != "" { + // Make sure the reviewer is listed in Rietveld as a reviewer. + url := codereviewBase + "/" + n + "/fields" + resp, err := urlfetch.Client(c).Get(url + "?field=reviewers") + if err != nil { + c.Errorf("Retrieving CL reviewer list failed: %v", err) + http.Error(w, err.Error(), 500) + return + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + c.Errorf("Retrieving CL reviewer list failed: got HTTP response %d", resp.StatusCode) + http.Error(w, "Failed contacting Rietveld", 500) + return + } + + var apiResp struct { + Reviewers []string `json:"reviewers"` + } + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + // probably can't be retried + msg := fmt.Sprintf("Malformed JSON from %v: %v", url, err) + c.Errorf("%s", msg) + http.Error(w, msg, 500) + return + } + found := false + for _, r := range apiResp.Reviewers { + if emailToPerson[r] == rev { + found = true + break + } + } + if !found { + c.Infof("Adding %v as a reviewer of CL %v", rev, n) + + url := fmt.Sprintf("%s?cl=%s&r=%s&obo=%s", gobotBase, n, rev, person) + resp, err := urlfetch.Client(c).Get(url) + if err != nil { + c.Errorf("Gobot GET failed: %v", err) + http.Error(w, err.Error(), 500) + return + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + c.Errorf("Gobot GET failed: got HTTP response %d", resp.StatusCode) + http.Error(w, "Failed contacting Gobot", 500) + return + } + + c.Infof("Gobot said %q", resp.Status) + } + } + + // Update our own record. + err := datastore.RunInTransaction(c, func(c appengine.Context) error { + cl := new(CL) + err := datastore.Get(c, key, cl) + if err != nil { + return err + } + cl.Reviewer = rev + _, err = datastore.Put(c, key, cl) + return err + }, nil) + if err != nil { + msg := fmt.Sprintf("Assignment failed: %v", err) + c.Errorf("%s", msg) + http.Error(w, msg, 500) + return + } + c.Infof("Assigned CL %v to %v", n, rev) +} + +func UpdateCLLater(c appengine.Context, n string, delay time.Duration) { + t := taskqueue.NewPOSTTask("/update-cl", url.Values{ + "cl": []string{n}, + }) + t.Delay = delay + if _, err := taskqueue.Add(c, t, "update-cl"); err != nil { + c.Errorf("Failed adding task: %v", err) + } +} + +func handleUpdateCL(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + + n := r.FormValue("cl") + if !clRegexp.MatchString(n) { + c.Errorf("Bad CL %q", n) + http.Error(w, "Bad CL", 400) + return + } + + if err := updateCL(c, n); err != nil { + c.Errorf("Failed updating CL %v: %v", n, err) + http.Error(w, "Failed update", 500) + return + } + + io.WriteString(w, "OK") +} + +// apiMessage describes the JSON sent back by Rietveld in the CL messages list. +type apiMessage struct { + Date string `json:"date"` + Text string `json:"text"` + Sender string `json:"sender"` + Recipients []string `json:"recipients"` + Approval bool `json:"approval"` +} + +// byDate implements sort.Interface to order the messages by date, earliest first. +// The dates are sent in RFC 3339 format, so string comparison matches time value comparison. +type byDate []*apiMessage + +func (x byDate) Len() int { return len(x) } +func (x byDate) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byDate) Less(i, j int) bool { return x[i].Date < x[j].Date } + +// updateCL updates a single CL. If a retryable failure occurs, an error is returned. +func updateCL(c appengine.Context, n string) error { + c.Debugf("Updating CL %v", n) + key := datastore.NewKey(c, "CL", n, 0, nil) + + url := codereviewBase + "/api/" + n + "?messages=true" + resp, err := urlfetch.Client(c).Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + raw, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Failed reading HTTP body: %v", err) + } + + // Special case for abandoned CLs. + if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) { + // Don't bother checking for errors. The CL might never have been saved, for instance. + datastore.Delete(c, key) + c.Infof("Deleted abandoned CL %v", n) + return nil + } + + if resp.StatusCode != 200 { + return fmt.Errorf("Update: got HTTP response %d", resp.StatusCode) + } + + var apiResp struct { + Description string `json:"description"` + Reviewers []string `json:"reviewers"` + Created string `json:"created"` + OwnerEmail string `json:"owner_email"` + Modified string `json:"modified"` + Closed bool `json:"closed"` + Subject string `json:"subject"` + Messages []*apiMessage `json:"messages"` + } + if err := json.Unmarshal(raw, &apiResp); err != nil { + // probably can't be retried + c.Errorf("Malformed JSON from %v: %v", url, err) + return nil + } + //c.Infof("RAW: %+v", apiResp) + sort.Sort(byDate(apiResp.Messages)) + + cl := &CL{ + Number: n, + Closed: apiResp.Closed, + Owner: apiResp.OwnerEmail, + Description: []byte(apiResp.Description), + FirstLine: apiResp.Description, + Subject: apiResp.Subject, + Author: emailToPerson[apiResp.OwnerEmail], + } + cl.Created, err = time.Parse("2006-01-02 15:04:05.000000", apiResp.Created) + if err != nil { + c.Errorf("Bad creation time %q: %v", apiResp.Created, err) + } + cl.Modified, err = time.Parse("2006-01-02 15:04:05.000000", apiResp.Modified) + if err != nil { + c.Errorf("Bad modification time %q: %v", apiResp.Modified, err) + } + if i := strings.Index(cl.FirstLine, "\n"); i >= 0 { + cl.FirstLine = cl.FirstLine[:i] + } + // Treat zero reviewers as a signal that the CL is completed. + // This could be after the CL has been submitted, but before the CL author has synced, + // but it could also be a CL manually edited to remove reviewers. + if len(apiResp.Reviewers) == 0 { + cl.Closed = true + } + + lgtm := make(map[string]bool) + notLGTM := make(map[string]bool) + rcpt := make(map[string]bool) + for _, msg := range apiResp.Messages { + s, rev := msg.Sender, false + if p, ok := emailToPerson[s]; ok { + s, rev = p, true + } + + line := firstLine(msg.Text) + if line != "" { + cl.LastUpdateBy = msg.Sender + cl.LastUpdate = line + } + + // CLs submitted by someone other than the CL owner do not immediately + // transition to "closed". Let's simulate the intention by treating + // messages starting with "*** Submitted as " from a reviewer as a + // signal that the CL is now closed. + if rev && strings.HasPrefix(msg.Text, "*** Submitted as ") { + cl.Closed = true + } + + if msg.Approval { + lgtm[s] = true + delete(notLGTM, s) // "LGTM" overrules previous "NOT LGTM" + } + if strings.Contains(line, "NOT LGTM") { + notLGTM[s] = true + delete(lgtm, s) // "NOT LGTM" overrules previous "LGTM" + } + + for _, r := range msg.Recipients { + rcpt[r] = true + } + } + for l := range lgtm { + cl.LGTMs = append(cl.LGTMs, l) + } + for l := range notLGTM { + cl.NotLGTMs = append(cl.NotLGTMs, l) + } + for r := range rcpt { + cl.Recipients = append(cl.Recipients, r) + } + sort.Strings(cl.LGTMs) + sort.Strings(cl.NotLGTMs) + sort.Strings(cl.Recipients) + + err = datastore.RunInTransaction(c, func(c appengine.Context) error { + ocl := new(CL) + err := datastore.Get(c, key, ocl) + if err != nil && err != datastore.ErrNoSuchEntity { + return err + } else if err == nil { + // LastMessageID and Reviewer need preserving. + cl.LastMessageID = ocl.LastMessageID + cl.Reviewer = ocl.Reviewer + } + _, err = datastore.Put(c, key, cl) + return err + }, nil) + if err != nil { + return err + } + c.Infof("Updated CL %v", n) + return nil +} + +// trailingSpaceRE matches trailing spaces. +var trailingSpaceRE = regexp.MustCompile(`(?m)[ \t\r]+$`) + +// removeRE is the list of patterns to skip over at the beginning of a +// message when looking for message text. +var removeRE = regexp.MustCompile(`(?m-s)\A(` + + // Skip leading "Hello so-and-so," generated by codereview plugin. + `(Hello(.|\n)*?\n\n)` + + + // Skip quoted text. + `|((On.*|.* writes|.* wrote):\n)` + + `|((>.*\n)+)` + + + // Skip lines with no letters. + `|(([^A-Za-z]*\n)+)` + + + // Skip links to comments and file info. + `|(http://codereview.*\n([^ ]+:[0-9]+:.*\n)?)` + + `|(File .*:\n)` + + + `)`, +) + +// firstLine returns the first interesting line of the message text. +func firstLine(text string) string { + // Cut trailing spaces. + text = trailingSpaceRE.ReplaceAllString(text, "") + + // Skip uninteresting lines. + for { + text = strings.TrimSpace(text) + m := removeRE.FindStringIndex(text) + if m == nil || m[0] != 0 { + break + } + text = text[m[1]:] + } + + // Chop line at newline or else at 74 bytes. + i := strings.Index(text, "\n") + if i >= 0 { + text = text[:i] + } + if len(text) > 74 { + text = text[:70] + "..." + } + return text +} diff --git a/misc/dashboard/codereview/dashboard/front.go b/misc/dashboard/codereview/dashboard/front.go new file mode 100644 index 000000000..c7b0f0fbf --- /dev/null +++ b/misc/dashboard/codereview/dashboard/front.go @@ -0,0 +1,299 @@ +// 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 dashboard + +// This file handles the front page. + +import ( + "bytes" + "html/template" + "io" + "net/http" + "strings" + "sync" + "time" + + "appengine" + "appengine/datastore" + "appengine/user" +) + +func init() { + http.HandleFunc("/", handleFront) + http.HandleFunc("/favicon.ico", http.NotFound) +} + +// maximum number of active CLs to show in person-specific tables. +const maxCLs = 100 + +func handleFront(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + + data := &frontPageData{ + Reviewers: personList, + User: user.Current(c).Email, + IsAdmin: user.IsAdmin(c), + } + var currentPerson string + u := data.User + you := "you" + if e := r.FormValue("user"); e != "" { + u = e + you = e + } + currentPerson, data.UserIsReviewer = emailToPerson[u] + if !data.UserIsReviewer { + currentPerson = u + } + + var wg sync.WaitGroup + errc := make(chan error, 10) + activeCLs := datastore.NewQuery("CL"). + Filter("Closed =", false). + Order("-Modified") + + tableFetch := func(index int, f func(tbl *clTable) error) { + wg.Add(1) + go func() { + defer wg.Done() + start := time.Now() + if err := f(&data.Tables[index]); err != nil { + errc <- err + } + data.Timing[index] = time.Now().Sub(start) + }() + } + + data.Tables[0].Title = "CLs assigned to " + you + " for review" + if data.UserIsReviewer { + tableFetch(0, func(tbl *clTable) error { + q := activeCLs.Filter("Reviewer =", currentPerson).Limit(maxCLs) + tbl.Assignable = true + _, err := q.GetAll(c, &tbl.CLs) + return err + }) + } + + tableFetch(1, func(tbl *clTable) error { + q := activeCLs + if data.UserIsReviewer { + q = q.Filter("Author =", currentPerson) + } else { + q = q.Filter("Owner =", currentPerson) + } + q = q.Limit(maxCLs) + tbl.Title = "CLs sent by " + you + tbl.Assignable = true + _, err := q.GetAll(c, &tbl.CLs) + return err + }) + + tableFetch(2, func(tbl *clTable) error { + q := activeCLs.Limit(50) + tbl.Title = "Other active CLs" + tbl.Assignable = true + if _, err := q.GetAll(c, &tbl.CLs); err != nil { + return err + } + // filter + for i := len(tbl.CLs) - 1; i >= 0; i-- { + cl := tbl.CLs[i] + if cl.Owner == currentPerson || cl.Author == currentPerson || cl.Reviewer == currentPerson { + // Preserve order. + copy(tbl.CLs[i:], tbl.CLs[i+1:]) + tbl.CLs = tbl.CLs[:len(tbl.CLs)-1] + } + } + return nil + }) + + tableFetch(3, func(tbl *clTable) error { + q := datastore.NewQuery("CL"). + Filter("Closed =", true). + Order("-Modified"). + Limit(10) + tbl.Title = "Recently closed CLs" + tbl.Assignable = false + _, err := q.GetAll(c, &tbl.CLs) + return err + }) + + // Not really a table fetch. + tableFetch(0, func(_ *clTable) error { + var err error + data.LogoutURL, err = user.LogoutURL(c, "/") + return err + }) + + wg.Wait() + + select { + case err := <-errc: + c.Errorf("%v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + default: + } + + var b bytes.Buffer + if err := frontPage.ExecuteTemplate(&b, "front", &data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + io.Copy(w, &b) +} + +type frontPageData struct { + Tables [4]clTable + Timing [4]time.Duration + + Reviewers []string + UserIsReviewer bool + + User, LogoutURL string // actual logged in user + IsAdmin bool +} + +type clTable struct { + Title string + Assignable bool + CLs []*CL +} + +var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{ + "selected": func(a, b string) string { + if a == b { + return "selected" + } + return "" + }, + "shortemail": func(s string) string { + if i := strings.Index(s, "@"); i >= 0 { + s = s[:i] + } + return s + }, +}).Parse(` +<!doctype html> +<html> + <head> + <title>Go code reviews</title> + <link rel="icon" type="image/png" href="/static/icon.png" /> + <style type="text/css"> + body { + font-family: Helvetica, sans-serif; + } + img#gopherstamp { + float: right; + height: auto; + width: 250px; + } + h1, h2, h3 { + color: #777; + margin-bottom: 0; + } + table { + border-spacing: 0; + } + td { + vertical-align: top; + padding: 2px 5px; + } + tr.unreplied td.email { + border-left: 2px solid blue; + } + tr.pending td { + background: #fc8; + } + tr.failed td { + background: #f88; + } + tr.saved td { + background: #8f8; + } + .cls { + margin-top: 0; + } + a { + color: blue; + text-decoration: none; /* no link underline */ + } + address { + font-size: 10px; + text-align: right; + } + .email { + font-family: monospace; + } + </style> + <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> + <head> + <body> + +<img id="gopherstamp" src="/static/gopherstamp.jpg" /> +<h1>Go code reviews</h1> + +<table class="cls"> +{{range $i, $tbl := .Tables}} +<tr><td colspan="5"><h3>{{$tbl.Title}}</h3></td></tr> +{{if .CLs}} +{{range $cl := .CLs}} + <tr id="cl-{{$cl.Number}}" class="{{if not $i}}{{if not .Reviewed}}unreplied{{end}}{{end}}"> + <td class="email">{{$cl.DisplayOwner}}</td> + <td> + {{if $tbl.Assignable}} + <select id="cl-rev-{{$cl.Number}}" {{if not $.UserIsReviewer}}disabled{{end}}> + <option></option> + {{range $.Reviewers}} + <option {{selected . $cl.Reviewer}}>{{.}}</option> + {{end}} + </select> + <script type="text/javascript"> + $(function() { + $('#cl-rev-{{$cl.Number}}').change(function() { + var r = $(this).val(); + var row = $('tr#cl-{{$cl.Number}}'); + row.addClass('pending'); + $.post('/assign', { + 'cl': '{{$cl.Number}}', + 'r': r + }).success(function() { + row.removeClass('pending'); + row.addClass('saved'); + }).error(function() { + row.removeClass('pending'); + row.addClass('failed'); + }); + }); + }); + </script> + {{end}} + </td> + <td> + <a href="http://codereview.appspot.com/{{.Number}}/" title="{{ printf "%s" .Description}}">{{.Number}}: {{.FirstLineHTML}}</a> + {{if and .LGTMs $tbl.Assignable}}<br /><span style="font-size: smaller;">LGTMs: {{.LGTMHTML}}</span>{{end}} + {{if and .NotLGTMs $tbl.Assignable}}<br /><span style="font-size: smaller; color: #f74545;">NOT LGTMs: {{.NotLGTMHTML}}</span>{{end}} + {{if .LastUpdateBy}}<br /><span style="font-size: smaller; color: #777777;">(<span title="{{.LastUpdateBy}}">{{.LastUpdateBy | shortemail}}</span>) {{.LastUpdate}}</span>{{end}} + </td> + <td title="Last modified">{{.ModifiedAgo}}</td> + <td>{{if $.IsAdmin}}<a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a>{{end}}</td> + </tr> +{{end}} +{{else}} +<tr><td colspan="5"><em>none</em></td></tr> +{{end}} +{{end}} +</table> + +<hr /> +<address> +You are <span class="email">{{.User}}</span> · <a href="{{.LogoutURL}}">logout</a><br /> +datastore timing: {{range .Timing}} {{.}}{{end}} +</address> + + </body> +</html> +`)) diff --git a/misc/dashboard/codereview/dashboard/gc.go b/misc/dashboard/codereview/dashboard/gc.go new file mode 100644 index 000000000..a80b375f6 --- /dev/null +++ b/misc/dashboard/codereview/dashboard/gc.go @@ -0,0 +1,47 @@ +// 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 dashboard + +// This file handles garbage collection of old CLs. + +import ( + "net/http" + "time" + + "appengine" + "appengine/datastore" +) + +func init() { + http.HandleFunc("/gc", handleGC) +} + +func handleGC(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + + // Delete closed CLs that haven't been modified in 168 hours (7 days). + cutoff := time.Now().Add(-168 * time.Hour) + q := datastore.NewQuery("CL"). + Filter("Closed =", true). + Filter("Modified <", cutoff). + Limit(100). + KeysOnly() + keys, err := q.GetAll(c, nil) + if err != nil { + c.Errorf("GetAll failed for old CLs: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(keys) == 0 { + return + } + + if err := datastore.DeleteMulti(c, keys); err != nil { + c.Errorf("DeleteMulti failed for old CLs: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + c.Infof("Deleted %d old CLs", len(keys)) +} diff --git a/misc/dashboard/codereview/dashboard/mail.go b/misc/dashboard/codereview/dashboard/mail.go new file mode 100644 index 000000000..838d08222 --- /dev/null +++ b/misc/dashboard/codereview/dashboard/mail.go @@ -0,0 +1,68 @@ +// 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 dashboard + +// This file handles receiving mail. + +import ( + "net/http" + "net/mail" + "regexp" + "time" + + "appengine" + "appengine/datastore" +) + +func init() { + http.HandleFunc("/_ah/mail/", handleMail) +} + +var subjectRegexp = regexp.MustCompile(`.*code review (\d+):.*`) + +func handleMail(w http.ResponseWriter, r *http.Request) { + c := appengine.NewContext(r) + + defer r.Body.Close() + msg, err := mail.ReadMessage(r.Body) + if err != nil { + c.Errorf("mail.ReadMessage: %v", err) + return + } + + subj := msg.Header.Get("Subject") + m := subjectRegexp.FindStringSubmatch(subj) + if len(m) != 2 { + c.Debugf("Subject %q did not match /%v/", subj, subjectRegexp) + return + } + + c.Infof("Found issue %q", m[1]) + + // Track the MessageID. + key := datastore.NewKey(c, "CL", m[1], 0, nil) + err = datastore.RunInTransaction(c, func(c appengine.Context) error { + cl := new(CL) + err := datastore.Get(c, key, cl) + if err != nil && err != datastore.ErrNoSuchEntity { + return err + } + if err == datastore.ErrNoSuchEntity { + // Must set sentinel values for time.Time fields + // if this is a new entity. + cl.Created = time.Unix(0, 0) + cl.Modified = time.Unix(0, 0) + } + cl.LastMessageID = msg.Header.Get("Message-ID") + _, err = datastore.Put(c, key, cl) + return err + }, nil) + if err != nil { + c.Errorf("datastore transaction failed: %v", err) + } + + // Update the CL after a delay to give Rietveld a chance to catch up. + UpdateCLLater(c, m[1], 10*time.Second) +} diff --git a/misc/dashboard/codereview/dashboard/people.go b/misc/dashboard/codereview/dashboard/people.go new file mode 100644 index 000000000..facda7baf --- /dev/null +++ b/misc/dashboard/codereview/dashboard/people.go @@ -0,0 +1,42 @@ +// 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 dashboard + +// This file handles identities of people. + +import ( + "sort" +) + +var ( + emailToPerson = make(map[string]string) // email => person + preferredEmail = make(map[string]string) // person => email + personList []string +) + +func init() { + // People we assume have golang.org and google.com accounts, + // and prefer to use their golang.org address for code review. + gophers := [...]string{ + "adg", + "bradfitz", + "campoy", + "dsymonds", + "gri", + "iant", + "nigeltao", + "r", + "rsc", + "sameer", + } + for _, p := range gophers { + personList = append(personList, p) + emailToPerson[p+"@golang.org"] = p + emailToPerson[p+"@google.com"] = p + preferredEmail[p] = p + "@golang.org" + } + + sort.Strings(personList) +} diff --git a/misc/dashboard/codereview/index.yaml b/misc/dashboard/codereview/index.yaml new file mode 100644 index 000000000..a87073cc4 --- /dev/null +++ b/misc/dashboard/codereview/index.yaml @@ -0,0 +1,25 @@ +indexes: + +- kind: CL + properties: + - name: Author + - name: Modified + direction: desc + +- kind: CL + properties: + - name: Owner + - name: Modified + direction: desc + +- kind: CL + properties: + - name: Closed + - name: Modified + direction: desc + +- kind: CL + properties: + - name: Reviewer + - name: Modified + direction: desc diff --git a/misc/dashboard/codereview/queue.yaml b/misc/dashboard/codereview/queue.yaml new file mode 100644 index 000000000..1a35facaf --- /dev/null +++ b/misc/dashboard/codereview/queue.yaml @@ -0,0 +1,4 @@ +queue: +- name: update-cl + rate: 12/m + bucket_size: 1 diff --git a/misc/dashboard/codereview/static/gopherstamp.jpg b/misc/dashboard/codereview/static/gopherstamp.jpg Binary files differnew file mode 100644 index 000000000..b17f3c82a --- /dev/null +++ b/misc/dashboard/codereview/static/gopherstamp.jpg diff --git a/misc/dashboard/codereview/static/icon.png b/misc/dashboard/codereview/static/icon.png Binary files differnew file mode 100644 index 000000000..c929ac8e9 --- /dev/null +++ b/misc/dashboard/codereview/static/icon.png diff --git a/misc/dist/bindist.go b/misc/dist/bindist.go index 16193bdc1..29454c73c 100644 --- a/misc/dist/bindist.go +++ b/misc/dist/bindist.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // This is a tool for packaging binary releases. -// It supports FreeBSD, Linux, OS X, and Windows. +// It supports FreeBSD, Linux, NetBSD, OS X, and Windows. package main import ( @@ -13,7 +13,6 @@ import ( "bytes" "compress/gzip" "encoding/base64" - "errors" "flag" "fmt" "io" @@ -30,8 +29,9 @@ import ( ) var ( - tag = flag.String("tag", "weekly", "mercurial tag to check out") + tag = flag.String("tag", "release", "mercurial tag to check out") repo = flag.String("repo", "https://code.google.com/p/go", "repo URL") + tourPath = flag.String("tour", "code.google.com/p/go-tour", "Go tour repo import path") verbose = flag.Bool("v", false, "verbose output") upload = flag.Bool("upload", true, "upload resulting files to Google Code") wxsFile = flag.String("wxs", "", "path to custom installer.wxs") @@ -41,8 +41,7 @@ var ( ) const ( - packageMaker = "/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker" - uploadURL = "https://go.googlecode.com/files" + uploadURL = "https://go.googlecode.com/files" ) var preBuildCleanFiles = []string{ @@ -66,6 +65,20 @@ var sourceCleanFiles = []string{ "pkg", } +var tourPackages = []string{ + "pic", + "tree", + "wc", +} + +var tourContent = []string{ + "prog", + "solutions", + "static", + "template", + "tour.article", +} + var fileRe = regexp.MustCompile(`^go\.([a-z0-9-.]+)\.(src|([a-z0-9]+)-([a-z0-9]+))\.`) func main() { @@ -129,6 +142,7 @@ type Build struct { OS string Arch string root string + gopath string } func (b *Build) Do() error { @@ -138,6 +152,7 @@ func (b *Build) Do() error { } defer os.RemoveAll(work) b.root = filepath.Join(work, "go") + b.gopath = work // Clone Go distribution and update to tag. _, err = b.run(work, "hg", "clone", "-q", *repo, b.root) @@ -173,6 +188,10 @@ func (b *Build) Do() error { return err } + if err := b.tour(); err != nil { + return err + } + // Get version strings. var ( version string // "weekly.2012-03-04" @@ -211,19 +230,31 @@ func (b *Build) Do() error { } // Create packages. - base := fmt.Sprintf("go.%s.%s-%s", version, b.OS, b.Arch) + base := fmt.Sprintf("%s.%s-%s", version, b.OS, b.Arch) + if !strings.HasPrefix(base, "go") { + base = "go." + base + } var targs []string switch b.OS { - case "linux", "freebsd", "": + case "linux", "freebsd", "netbsd", "": // build tarball targ := base if b.Source { - targ = fmt.Sprintf("go.%s.src", version) + targ = fmt.Sprintf("%s.src", version) + if !strings.HasPrefix(targ, "go") { + targ = "go." + targ + } } targ += ".tar.gz" err = makeTar(targ, work) targs = append(targs, targ) case "darwin": + // build tarball + targ := base + ".tar.gz" + err = makeTar(targ, work) + targs = append(targs, targ) + + // build pkg // arrange work so it's laid out as the dest filesystem etc := filepath.Join(b.root, "misc/dist/darwin/etc") _, err = b.run(work, "cp", "-r", etc, ".") @@ -231,7 +262,7 @@ func (b *Build) Do() error { return err } localDir := filepath.Join(work, "usr/local") - err = os.MkdirAll(localDir, 0744) + err = os.MkdirAll(localDir, 0755) if err != nil { return err } @@ -240,23 +271,30 @@ func (b *Build) Do() error { return err } // build package - pm := packageMaker - if !exists(pm) { - pm = "/Developer" + pm - if !exists(pm) { - return errors.New("couldn't find PackageMaker") - } + pkgdest, err := ioutil.TempDir("", "pkgdest") + if err != nil { + return err } - targ := base + ".pkg" - scripts := filepath.Join(work, "usr/local/go/misc/dist/darwin/scripts") - _, err = b.run("", pm, "-v", - "-r", work, - "-o", targ, - "--scripts", scripts, - "--id", "com.googlecode.go", - "--title", "Go", + defer os.RemoveAll(pkgdest) + dist := filepath.Join(runtime.GOROOT(), "misc/dist") + _, err = b.run("", "pkgbuild", + "--identifier", "com.googlecode.go", "--version", "1.0", - "--target", "10.6") + "--scripts", filepath.Join(dist, "darwin/scripts"), + "--root", work, + filepath.Join(pkgdest, "com.googlecode.go.pkg")) + if err != nil { + return err + } + targ = base + ".pkg" + _, err = b.run("", "productbuild", + "--distribution", filepath.Join(dist, "darwin/Distribution"), + "--resources", filepath.Join(dist, "darwin/Resources"), + "--package-path", pkgdest, + targ) + if err != nil { + return err + } targs = append(targs, targ) case "windows": // Create ZIP file. @@ -327,6 +365,33 @@ func (b *Build) Do() error { return err } +func (b *Build) tour() error { + // go get the gotour package. + _, err := b.run(b.gopath, filepath.Join(b.root, "bin", "go"), "get", *tourPath+"/gotour") + if err != nil { + return err + } + + // Copy all the tour content to $GOROOT/misc/tour. + importPath := filepath.FromSlash(*tourPath) + tourSrc := filepath.Join(b.gopath, "src", importPath) + contentDir := filepath.Join(b.root, "misc", "tour") + if err = cpAllDir(contentDir, tourSrc, tourContent...); err != nil { + return err + } + + // Copy the tour source code so it's accessible with $GOPATH pointing to $GOROOT/misc/tour. + if err = cpAllDir(filepath.Join(contentDir, "src", importPath), tourSrc, tourPackages...); err != nil { + return err + } + + // Copy gotour binary to tool directory as "tour"; invoked as "go tool tour". + return cp( + filepath.Join(b.root, "pkg", "tool", b.OS+"_"+b.Arch, "tour"), + filepath.Join(b.gopath, "bin", "gotour"), + ) +} + func (b *Build) run(dir, name string, args ...string) ([]byte, error) { buf := new(bytes.Buffer) absName, err := lookPath(name) @@ -358,6 +423,7 @@ var cleanEnv = []string{ "GOOS", "GOROOT", "GOROOT_FINAL", + "GOPATH", } func (b *Build) env() []string { @@ -380,6 +446,7 @@ func (b *Build) env() []string { "GOOS="+b.OS, "GOROOT="+b.root, "GOROOT_FINAL="+final, + "GOPATH="+b.gopath, ) return env } @@ -390,42 +457,51 @@ func (b *Build) Upload(version string, filename string) error { os_, arch := b.OS, b.Arch switch b.Arch { case "386": - arch = "32-bit" + arch = "x86 32-bit" case "amd64": - arch = "64-bit" + arch = "x86 64-bit" } if arch != "" { labels = append(labels, "Arch-"+b.Arch) } + var opsys, ftype string // labels switch b.OS { case "linux": os_ = "Linux" - labels = append(labels, "Type-Archive", "OpSys-Linux") + opsys = "Linux" case "freebsd": os_ = "FreeBSD" - labels = append(labels, "Type-Archive", "OpSys-FreeBSD") + opsys = "FreeBSD" case "darwin": os_ = "Mac OS X" - labels = append(labels, "Type-Installer", "OpSys-OSX") + opsys = "OSX" + case "netbsd": + os_ = "NetBSD" + opsys = "NetBSD" case "windows": os_ = "Windows" - labels = append(labels, "OpSys-Windows") + opsys = "Windows" } summary := fmt.Sprintf("%s %s (%s)", version, os_, arch) - if b.OS == "windows" { - switch { - case strings.HasSuffix(filename, ".msi"): - labels = append(labels, "Type-Installer") - summary += " MSI installer" - case strings.HasSuffix(filename, ".zip"): - labels = append(labels, "Type-Archive") - summary += " ZIP archive" - } + switch { + case strings.HasSuffix(filename, ".msi"): + ftype = "Installer" + summary += " MSI installer" + case strings.HasSuffix(filename, ".pkg"): + ftype = "Installer" + summary += " PKG installer" + case strings.HasSuffix(filename, ".zip"): + ftype = "Archive" + summary += " ZIP archive" + case strings.HasSuffix(filename, ".tar.gz"): + ftype = "Archive" + summary += " tarball" } if b.Source { - labels = append(labels, "Type-Source") + ftype = "Source" summary = fmt.Sprintf("%s (source only)", version) } + labels = append(labels, "OpSys-"+opsys, "Type-"+ftype) if *addLabel != "" { labels = append(labels, *addLabel) } @@ -535,15 +611,45 @@ func cp(dst, src string) error { return err } defer sf.Close() + fi, err := sf.Stat() + if err != nil { + return err + } df, err := os.Create(dst) if err != nil { return err } defer df.Close() + if err := df.Chmod(fi.Mode()); err != nil { + return err + } _, err = io.Copy(df, sf) return err } +func cpDir(dst, src string) error { + walk := func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + dstPath := filepath.Join(dst, srcPath[len(src):]) + if info.IsDir() { + return os.MkdirAll(dstPath, 0755) + } + return cp(dstPath, srcPath) + } + return filepath.Walk(src, walk) +} + +func cpAllDir(dst, basePath string, dirs ...string) error { + for _, dir := range dirs { + if err := cpDir(filepath.Join(dst, dir), filepath.Join(basePath, dir)); err != nil { + return err + } + } + return nil +} + func makeTar(targ, workdir string) error { f, err := os.Create(targ) if err != nil { @@ -552,7 +658,7 @@ func makeTar(targ, workdir string) error { zout := gzip.NewWriter(f) tw := tar.NewWriter(zout) - filepath.Walk(workdir, filepath.WalkFunc(func(path string, fi os.FileInfo, err error) error { + err = filepath.Walk(workdir, func(path string, fi os.FileInfo, err error) error { if !strings.HasPrefix(path, workdir) { log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir) } @@ -570,10 +676,8 @@ func makeTar(targ, workdir string) error { if *verbose { log.Printf("adding to tar: %s", name) } - if fi.IsDir() { - return nil - } - hdr, err := tarFileInfoHeader(fi, path) + target, _ := os.Readlink(path) + hdr, err := tar.FileInfoHeader(fi, target) if err != nil { return err } @@ -594,6 +698,9 @@ func makeTar(targ, workdir string) error { if err != nil { return fmt.Errorf("Error writing file %q: %v", name, err) } + if fi.IsDir() { + return nil + } r, err := os.Open(path) if err != nil { return err @@ -601,8 +708,10 @@ func makeTar(targ, workdir string) error { defer r.Close() _, err = io.Copy(tw, r) return err - })) - + }) + if err != nil { + return err + } if err := tw.Close(); err != nil { return err } @@ -619,10 +728,7 @@ func makeZip(targ, workdir string) error { } zw := zip.NewWriter(f) - filepath.Walk(workdir, filepath.WalkFunc(func(path string, fi os.FileInfo, err error) error { - if fi.IsDir() { - return nil - } + err = filepath.Walk(workdir, func(path string, fi os.FileInfo, err error) error { if !strings.HasPrefix(path, workdir) { log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir) } @@ -649,10 +755,17 @@ func makeZip(targ, workdir string) error { } fh.Name = name fh.Method = zip.Deflate + if fi.IsDir() { + fh.Name += "/" // append trailing slash + fh.Method = zip.Store // no need to deflate 0 byte files + } w, err := zw.CreateHeader(fh) if err != nil { return err } + if fi.IsDir() { + return nil + } r, err := os.Open(path) if err != nil { return err @@ -660,8 +773,10 @@ func makeZip(targ, workdir string) error { defer r.Close() _, err = io.Copy(w, r) return err - })) - + }) + if err != nil { + return err + } if err := zw.Close(); err != nil { return err } @@ -733,64 +848,3 @@ func lookPath(prog string) (absPath string, err error) { } return } - -// sysStat, if non-nil, populates h from system-dependent fields of fi. -var sysStat func(fi os.FileInfo, h *tar.Header) error - -// Mode constants from the tar spec. -const ( - c_ISDIR = 040000 - c_ISFIFO = 010000 - c_ISREG = 0100000 - c_ISLNK = 0120000 - c_ISBLK = 060000 - c_ISCHR = 020000 - c_ISSOCK = 0140000 -) - -// tarFileInfoHeader creates a partially-populated Header from an os.FileInfo. -// The filename parameter is used only in the case of symlinks, to call os.Readlink. -// If fi is a symlink but filename is empty, an error is returned. -func tarFileInfoHeader(fi os.FileInfo, filename string) (*tar.Header, error) { - h := &tar.Header{ - Name: fi.Name(), - ModTime: fi.ModTime(), - Mode: int64(fi.Mode().Perm()), // or'd with c_IS* constants later - } - switch { - case fi.Mode()&os.ModeType == 0: - h.Mode |= c_ISREG - h.Typeflag = tar.TypeReg - h.Size = fi.Size() - case fi.IsDir(): - h.Typeflag = tar.TypeDir - h.Mode |= c_ISDIR - case fi.Mode()&os.ModeSymlink != 0: - h.Typeflag = tar.TypeSymlink - h.Mode |= c_ISLNK - if filename == "" { - return h, fmt.Errorf("archive/tar: unable to populate Header.Linkname of symlinks") - } - targ, err := os.Readlink(filename) - if err != nil { - return h, err - } - h.Linkname = targ - case fi.Mode()&os.ModeDevice != 0: - if fi.Mode()&os.ModeCharDevice != 0 { - h.Mode |= c_ISCHR - h.Typeflag = tar.TypeChar - } else { - h.Mode |= c_ISBLK - h.Typeflag = tar.TypeBlock - } - case fi.Mode()&os.ModeSocket != 0: - h.Mode |= c_ISSOCK - default: - return nil, fmt.Errorf("archive/tar: unknown file mode %v", fi.Mode()) - } - if sysStat != nil { - return h, sysStat(fi, h) - } - return h, nil -} diff --git a/misc/dist/darwin/Distribution b/misc/dist/darwin/Distribution new file mode 100644 index 000000000..8b764b69f --- /dev/null +++ b/misc/dist/darwin/Distribution @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?> +<installer-script minSpecVersion="1.000000"> + <title>Go</title> + <background mime-type="image/png" file="bg.png"/> + <options customize="never" allow-external-scripts="no"/> + <domains enable_localSystem="true" /> + <installation-check script="installCheck();"/> + <script> +function installCheck() { + if(!(system.compareVersions(system.version.ProductVersion, '10.6.0') >= 0)) { + my.result.title = 'Unable to install'; + my.result.message = 'Go requires Mac OS X 10.6 or later.'; + my.result.type = 'Fatal'; + return false; + } + if(system.files.fileExistsAtPath('/usr/local/go/bin/go')) { + my.result.title = 'Previous Installation Detected'; + my.result.message = 'A previous installation of Go exists at /usr/local/go. This installer will remove the previous installation prior to installing. Please back up any data before proceeding.'; + my.result.type = 'Warning'; + return false; + } + return true; +} + </script> + <choices-outline> + <line choice="com.googlecode.go.choice"/> + </choices-outline> + <choice id="com.googlecode.go.choice" title="Go"> + <pkg-ref id="com.googlecode.go.pkg"/> + </choice> + <pkg-ref id="com.googlecode.go.pkg" auth="Root">com.googlecode.go.pkg</pkg-ref> +</installer-script> diff --git a/misc/dist/darwin/Resources/bg.png b/misc/dist/darwin/Resources/bg.png Binary files differnew file mode 100644 index 000000000..c3d8ea93a --- /dev/null +++ b/misc/dist/darwin/Resources/bg.png diff --git a/misc/dist/darwin/scripts/postinstall b/misc/dist/darwin/scripts/postinstall index 3748721c7..4410a3004 100644..100755 --- a/misc/dist/darwin/scripts/postinstall +++ b/misc/dist/darwin/scripts/postinstall @@ -9,14 +9,9 @@ find bin -exec chmod ugo+rx \{\} \; find . -type d -exec chmod ugo+rx \{\} \; chmod o-w . -echo "Fixing debuggers via sudo.bash" -# setgrp procmod the debuggers (sudo.bash) -cd $GOROOT/src -./sudo.bash - echo "Installing miscellaneous files:" XCODE_MISC_DIR="/Library/Application Support/Developer/Shared/Xcode/Specifications/" -if [ -f $XCODE_MISC_DIR ]; then +if [ -d "$XCODE_MISC_DIR" ]; then echo " XCode" cp $GOROOT/misc/xcode/* $XCODE_MISC_DIR fi diff --git a/misc/dist/darwin/scripts/preinstall b/misc/dist/darwin/scripts/preinstall new file mode 100644 index 000000000..4cdaaa4bc --- /dev/null +++ b/misc/dist/darwin/scripts/preinstall @@ -0,0 +1,8 @@ +#!/bin/bash + +GOROOT=/usr/local/go + +echo "Removing previous installation" +if [ -d $GOROOT ]; then + rm -r $GOROOT +fi diff --git a/misc/dist/stat_darwin.go b/misc/dist/stat_darwin.go deleted file mode 100644 index eb3f76a27..000000000 --- a/misc/dist/stat_darwin.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 - -package main - -import ( - "archive/tar" - "os" - "syscall" - "time" -) - -func init() { - sysStat = func(fi os.FileInfo, h *tar.Header) error { - sys, ok := fi.Sys().(*syscall.Stat_t) - if !ok { - return nil - } - h.Uid = int(sys.Uid) - h.Gid = int(sys.Gid) - // TODO(bradfitz): populate username & group. os/user - // doesn't cache LookupId lookups, and lacks group - // lookup functions. - h.AccessTime = time.Unix(sys.Atimespec.Unix()) - h.ChangeTime = time.Unix(sys.Ctimespec.Unix()) - // TODO(bradfitz): major/minor device numbers? - return nil - } -} diff --git a/misc/dist/stat_linux.go b/misc/dist/stat_linux.go deleted file mode 100644 index 0ddb8a3bf..000000000 --- a/misc/dist/stat_linux.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 - -package main - -import ( - "archive/tar" - "os" - "syscall" - "time" -) - -func init() { - sysStat = func(fi os.FileInfo, h *tar.Header) error { - sys, ok := fi.Sys().(*syscall.Stat_t) - if !ok { - return nil - } - h.Uid = int(sys.Uid) - h.Gid = int(sys.Gid) - // TODO(bradfitz): populate username & group. os/user - // doesn't cache LookupId lookups, and lacks group - // lookup functions. - h.AccessTime = time.Unix(sys.Atim.Unix()) - h.ChangeTime = time.Unix(sys.Ctim.Unix()) - // TODO(bradfitz): major/minor device numbers? - return nil - } -} diff --git a/misc/emacs/go-mode-load.el b/misc/emacs/go-mode-load.el index d453166a4..3fc35c116 100644 --- a/misc/emacs/go-mode-load.el +++ b/misc/emacs/go-mode-load.el @@ -1,50 +1,96 @@ -;;; go-mode-load.el --- Major mode for the Go programming language - +;;; go-mode-load.el --- automatically extracted autoloads ;;; Commentary: ;; To install go-mode, add the following lines to your .emacs file: ;; (add-to-list 'load-path "PATH CONTAINING go-mode-load.el" t) ;; (require 'go-mode-load) +;; ;; After this, go-mode will be used for files ending in '.go'. - +;; ;; To compile go-mode from the command line, run the following ;; emacs -batch -f batch-byte-compile go-mode.el - +;; ;; See go-mode.el for documentation. - -;;; Code: - +;; ;; To update this file, evaluate the following form ;; (let ((generated-autoload-file buffer-file-name)) (update-file-autoloads "go-mode.el")) +;;; Code: + -;;;### (autoloads (gofmt-before-save gofmt go-mode) "go-mode" "go-mode.el" -;;;;;; (19917 17808)) +;;;### (autoloads (go-download-play godoc gofmt-before-save go-mode) +;;;;;; "go-mode" "go-mode.el" (20767 50749)) ;;; Generated autoloads from go-mode.el (autoload 'go-mode "go-mode" "\ Major mode for editing Go source text. -This provides basic syntax highlighting for keywords, built-ins, -functions, and some types. It also provides indentation that is -\(almost) identical to gofmt. +This mode provides (not just) basic editing capabilities for +working with Go code. It offers almost complete syntax +highlighting, indentation that is almost identical to gofmt, +proper parsing of the buffer content to allow features such as +navigation by function, manipulation of comments or detection of +strings. + +Additionally to these core features, it offers various features to +help with writing Go code. You can directly run buffer content +through gofmt, read godoc documentation from within Emacs, modify +and clean up the list of package imports or interact with the +Playground (uploading and downloading pastes). + +The following extra functions are defined: + +- `gofmt' +- `godoc' +- `go-import-add' +- `go-remove-unused-imports' +- `go-goto-imports' +- `go-play-buffer' and `go-play-region' +- `go-download-play' + +If you want to automatically run `gofmt' before saving a file, +add the following hook to your emacs configuration: + +\(add-hook 'before-save-hook 'gofmt-before-save) + +If you're looking for even more integration with Go, namely +on-the-fly syntax checking, auto-completion and snippets, it is +recommended to look at goflymake +\(https://github.com/dougm/goflymake), gocode +\(https://github.com/nsf/gocode) and yasnippet-go +\(https://github.com/dominikh/yasnippet-go) \(fn)" t nil) -(add-to-list 'auto-mode-alist (cons "\\.go$" #'go-mode)) - -(autoload 'gofmt "go-mode" "\ -Pipe the current buffer through the external tool `gofmt`. -Replace the current buffer on success; display errors on failure. - -\(fn)" t nil) +(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode)) (autoload 'gofmt-before-save "go-mode" "\ Add this to .emacs to run gofmt on the current buffer when saving: - (add-hook 'before-save-hook #'gofmt-before-save) + (add-hook 'before-save-hook 'gofmt-before-save). + +Note that this will cause go-mode to get loaded the first time +you save any file, kind of defeating the point of autoloading. \(fn)" t nil) -;;;*** +(autoload 'godoc "go-mode" "\ +Show go documentation for a query, much like M-x man. + +\(fn QUERY)" t nil) +(autoload 'go-download-play "go-mode" "\ +Downloads a paste from the playground and inserts it in a Go +buffer. Tries to look for a URL at point. + +\(fn URL)" t nil) + +;;;*** + (provide 'go-mode-load) +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8 +;; End: +;;; go-mode-load.el ends here diff --git a/misc/emacs/go-mode.el b/misc/emacs/go-mode.el index 214c19685..8a16d8a4f 100644 --- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -1,40 +1,54 @@ ;;; go-mode.el --- Major mode for the Go programming language -;;; Commentary: +;; Copyright 2013 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. -;; For installation instructions, see go-mode-load.el +(require 'cl) +(require 'diff-mode) +(require 'ffap) +(require 'find-lisp) +(require 'url) -;;; To do: +(defconst go-dangling-operators-regexp "[^-]-\\|[^+]\\+\\|[/*&><.=|^]") +(defconst gofmt-stdin-tag "<standard input>") +(defconst go-identifier-regexp "[[:word:][:multibyte:]_]+") +(defconst go-label-regexp go-identifier-regexp) +(defconst go-type-regexp "[[:word:][:multibyte:]_*]+") +(defconst go-func-regexp (concat "\\<func\\>\\s *\\(" go-identifier-regexp "\\)")) +(defconst go-func-meth-regexp (concat "\\<func\\>\\s *\\(?:(\\s *" go-identifier-regexp "\\s +" go-type-regexp "\\s *)\\s *\\)?\\(" go-identifier-regexp "\\)(")) +(defconst go-builtins + '("append" "cap" "close" "complex" "copy" + "delete" "imag" "len" "make" "new" + "panic" "print" "println" "real" "recover") + "All built-in functions in the Go language. Used for font locking.") + +(defconst go-mode-keywords + '("break" "default" "func" "interface" "select" + "case" "defer" "go" "map" "struct" + "chan" "else" "goto" "package" "switch" + "const" "fallthrough" "if" "range" "type" + "continue" "for" "import" "return" "var") + "All keywords in the Go language. Used for font locking.") -;; * Indentation is *almost* identical to gofmt -;; ** We think struct literal keys are labels and outdent them -;; ** We disagree on the indentation of function literals in arguments -;; ** There are bugs with the close brace of struct literals -;; * Highlight identifiers according to their syntactic context: type, -;; variable, function call, or tag -;; * Command for adding an import -;; ** Check if it's already there -;; ** Factor/unfactor the import line -;; ** Alphabetize -;; * Remove unused imports -;; ** This is hard, since I have to be aware of shadowing to do it -;; right -;; * Format region using gofmt +(defconst go-constants '("nil" "true" "false" "iota")) +(defconst go-type-name-regexp (concat "\\(?:[*(]\\)*\\(?:" go-identifier-regexp "\\.\\)?\\(" go-identifier-regexp "\\)")) -;;; Code: +(defvar go-dangling-cache) -(eval-when-compile (require 'cl)) +(defgroup go nil + "Major mode for editing Go code" + :group 'languages) + +(defcustom go-fontify-function-calls t + "Fontify function and method calls if this is non-nil." + :type 'boolean + :group 'go) (defvar go-mode-syntax-table (let ((st (make-syntax-table))) - ;; Add _ to :word: character class - (modify-syntax-entry ?_ "w" st) - - ;; Operators (punctuation) (modify-syntax-entry ?+ "." st) (modify-syntax-entry ?- "." st) - (modify-syntax-entry ?* ". 23" st) ; also part of comments - (modify-syntax-entry ?/ (if (featurep 'xemacs) ". 1456" ". 124b") st) ; ditto (modify-syntax-entry ?% "." st) (modify-syntax-entry ?& "." st) (modify-syntax-entry ?| "." st) @@ -43,80 +57,56 @@ (modify-syntax-entry ?= "." st) (modify-syntax-entry ?< "." st) (modify-syntax-entry ?> "." st) - - ;; Strings and comments are font-locked separately. - (modify-syntax-entry ?\" "." st) - (modify-syntax-entry ?\' "." st) - (modify-syntax-entry ?` "." st) - (modify-syntax-entry ?\\ "." st) - - ;; Newline is a comment-ender. + (modify-syntax-entry ?/ ". 124b" st) + (modify-syntax-entry ?* ". 23" st) (modify-syntax-entry ?\n "> b" st) + (modify-syntax-entry ?\" "\"" st) + (modify-syntax-entry ?\' "\"" st) + (modify-syntax-entry ?` "\"" st) + (modify-syntax-entry ?\\ "\\" st) + (modify-syntax-entry ?_ "_" st) st) "Syntax table for Go mode.") -(defvar go-mode-keywords - '("break" "default" "func" "interface" "select" - "case" "defer" "go" "map" "struct" - "chan" "else" "goto" "package" "switch" - "const" "fallthrough" "if" "range" "type" - "continue" "for" "import" "return" "var") - "All keywords in the Go language. Used for font locking and -some syntax analysis.") - -(defvar go-mode-font-lock-keywords - (let ((builtins '("append" "cap" "close" "complex" "copy" "delete" "imag" "len" - "make" "new" "panic" "print" "println" "real" "recover")) - (constants '("nil" "true" "false" "iota")) - (type-name "\\s *\\(?:[*(]\\s *\\)*\\(?:\\w+\\s *\\.\\s *\\)?\\(\\w+\\)") - ) - `((go-mode-font-lock-cs-comment 0 font-lock-comment-face t) - (go-mode-font-lock-cs-string 0 font-lock-string-face t) - (,(regexp-opt go-mode-keywords 'words) . font-lock-keyword-face) - (,(regexp-opt builtins 'words) . font-lock-builtin-face) - (,(regexp-opt constants 'words) . font-lock-constant-face) - ;; Function names in declarations - ("\\<func\\>\\s *\\(\\w+\\)" 1 font-lock-function-name-face) - ;; Function names in methods are handled by function call pattern - ;; Function names in calls - ;; XXX Doesn't match if function name is surrounded by parens - ("\\(\\w+\\)\\s *(" 1 font-lock-function-name-face) - ;; Type names - ("\\<type\\>\\s *\\(\\w+\\)" 1 font-lock-type-face) - (,(concat "\\<type\\>\\s *\\w+\\s *" type-name) 1 font-lock-type-face) - ;; Arrays/slices/map value type - ;; XXX Wrong. Marks 0 in expression "foo[0] * x" - ;; (,(concat "]" type-name) 1 font-lock-type-face) - ;; Map key type - (,(concat "\\<map\\s *\\[" type-name) 1 font-lock-type-face) - ;; Channel value type - (,(concat "\\<chan\\>\\s *\\(?:<-\\)?" type-name) 1 font-lock-type-face) - ;; new/make type - (,(concat "\\<\\(?:new\\|make\\)\\>\\(?:\\s \\|)\\)*(" type-name) 1 font-lock-type-face) - ;; Type conversion - (,(concat "\\.\\s *(" type-name) 1 font-lock-type-face) - ;; Method receiver type - (,(concat "\\<func\\>\\s *(\\w+\\s +" type-name) 1 font-lock-type-face) - ;; Labels - ;; XXX Not quite right. Also marks compound literal fields. - ("^\\s *\\(\\w+\\)\\s *:\\(\\S.\\|$\\)" 1 font-lock-constant-face) - ("\\<\\(goto\\|break\\|continue\\)\\>\\s *\\(\\w+\\)" 2 font-lock-constant-face))) - "Basic font lock keywords for Go mode. Highlights keywords, -built-ins, functions, and some types.") - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Key map -;; +(defun go--build-font-lock-keywords () + (append + `((,(regexp-opt go-mode-keywords 'symbols) . font-lock-keyword-face) + (,(regexp-opt go-builtins 'symbols) . font-lock-builtin-face) + (,(regexp-opt go-constants 'symbols) . font-lock-constant-face) + (,go-func-regexp 1 font-lock-function-name-face)) ;; function (not method) name + + (if go-fontify-function-calls + `((,(concat "\\(" go-identifier-regexp "\\)[[:space:]]*(") 1 font-lock-function-name-face) ;; function call/method name + (,(concat "(\\(" go-identifier-regexp "\\))[[:space:]]*(") 1 font-lock-function-name-face)) ;; bracketed function call + `((,go-func-meth-regexp 1 font-lock-function-name-face))) ;; method name + + `( + ("\\<type\\>[[:space:]]*\\([^[:space:]]+\\)" 1 font-lock-type-face) ;; types + (,(concat "\\<type\\>[[:space:]]*" go-identifier-regexp "[[:space:]]*" go-type-name-regexp) 1 font-lock-type-face) ;; types + (,(concat "\\(?:[[:space:]]+\\|\\]\\)\\[\\([[:digit:]]+\\|\\.\\.\\.\\)?\\]" go-type-name-regexp) 2 font-lock-type-face) ;; Arrays/slices + (,(concat "map\\[[^]]+\\]" go-type-name-regexp) 1 font-lock-type-face) ;; map value type + (,(concat "\\(" go-identifier-regexp "\\)" "{") 1 font-lock-type-face) + (,(concat "\\<map\\[" go-type-name-regexp) 1 font-lock-type-face) ;; map key type + (,(concat "\\<chan\\>[[:space:]]*\\(?:<-\\)?" go-type-name-regexp) 1 font-lock-type-face) ;; channel type + (,(concat "\\<\\(?:new\\|make\\)\\>\\(?:[[:space:]]\\|)\\)*(" go-type-name-regexp) 1 font-lock-type-face) ;; new/make type + ;; TODO do we actually need this one or isn't it just a function call? + (,(concat "\\.\\s *(" go-type-name-regexp) 1 font-lock-type-face) ;; Type conversion + (,(concat "\\<func\\>[[:space:]]+(" go-identifier-regexp "[[:space:]]+" go-type-name-regexp ")") 1 font-lock-type-face) ;; Method receiver + ;; Like the original go-mode this also marks compound literal + ;; fields. There, it was marked as to fix, but I grew quite + ;; accustomed to it, so it'll stay for now. + (,(concat "^[[:space:]]*\\(" go-label-regexp "\\)[[:space:]]*:\\(\\S.\\|$\\)") 1 font-lock-constant-face) ;; Labels and compound literal fields + (,(concat "\\<\\(goto\\|break\\|continue\\)\\>[[:space:]]*\\(" go-label-regexp "\\)") 2 font-lock-constant-face)))) ;; labels in goto/break/continue (defvar go-mode-map (let ((m (make-sparse-keymap))) - (define-key m "}" #'go-mode-insert-and-indent) - (define-key m ")" #'go-mode-insert-and-indent) - (define-key m "," #'go-mode-insert-and-indent) - (define-key m ":" #'go-mode-delayed-electric) - ;; In case we get : indentation wrong, correct ourselves - (define-key m "=" #'go-mode-insert-and-indent) + (define-key m "}" 'go-mode-insert-and-indent) + (define-key m ")" 'go-mode-insert-and-indent) + (define-key m "," 'go-mode-insert-and-indent) + (define-key m ":" 'go-mode-insert-and-indent) + (define-key m "=" 'go-mode-insert-and-indent) + (define-key m (kbd "C-c C-a") 'go-import-add) m) "Keymap used by Go mode to implement electric keys.") @@ -127,606 +117,229 @@ built-ins, functions, and some types.") (call-interactively (lookup-key (current-global-map) key)) (indent-according-to-mode)) -(defvar go-mode-delayed-point nil - "The point following the previous insertion if the insertion -was a delayed electric key. Used to communicate between -`go-mode-delayed-electric' and `go-mode-delayed-electric-hook'.") -(make-variable-buffer-local 'go-mode-delayed-point) - -(defun go-mode-delayed-electric (p) - "Perform electric insertion, but delayed by one event. - -This inserts P into the buffer, as usual, then waits for another key. -If that second key causes a buffer modification starting at the -point after the insertion of P, reindents the line containing P." - - (interactive "p") - (self-insert-command p) - (setq go-mode-delayed-point (point))) - -(defun go-mode-delayed-electric-hook (b e l) - "An after-change-function that implements `go-mode-delayed-electric'." - - (when (and go-mode-delayed-point - (= go-mode-delayed-point b)) - (save-excursion - (save-match-data - (goto-char go-mode-delayed-point) - (indent-according-to-mode)))) - (setq go-mode-delayed-point nil)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Parser -;; - -(defvar go-mode-mark-cs-end 1 - "The point at which the comment/string cache ends. The buffer -will be marked from the beginning up to this point (that is, up -to and including character (1- go-mode-mark-cs-end)).") -(make-variable-buffer-local 'go-mode-mark-cs-end) - -(defvar go-mode-mark-string-end 1 - "The point at which the string cache ends. The buffer -will be marked from the beginning up to this point (that is, up -to and including character (1- go-mode-mark-string-end)).") -(make-variable-buffer-local 'go-mode-mark-string-end) - -(defvar go-mode-mark-comment-end 1 - "The point at which the comment cache ends. The buffer -will be marked from the beginning up to this point (that is, up -to and including character (1- go-mode-mark-comment-end)).") -(make-variable-buffer-local 'go-mode-mark-comment-end) - -(defvar go-mode-mark-nesting-end 1 - "The point at which the nesting cache ends. The buffer will be -marked from the beginning up to this point.") -(make-variable-buffer-local 'go-mode-mark-nesting-end) - -(defun go-mode-mark-clear-cs (b e l) - "An after-change-function that removes the go-mode-cs text property" - (remove-text-properties b e '(go-mode-cs))) - -(defun go-mode-mark-clear-cache (b e) - "A before-change-function that clears the comment/string and -nesting caches from the modified point on." - - (save-restriction - (widen) - (when (<= b go-mode-mark-cs-end) - ;; Remove the property adjacent to the change position. - ;; It may contain positions pointing beyond the new end mark. - (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-cs))) - (if cs (car cs) b)))) - (remove-text-properties - b (min go-mode-mark-cs-end (point-max)) '(go-mode-cs nil)) - (setq go-mode-mark-cs-end b))) - - (when (<= b go-mode-mark-string-end) - ;; Remove the property adjacent to the change position. - ;; It may contain positions pointing beyond the new end mark. - (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-string))) - (if cs (car cs) b)))) - (remove-text-properties - b (min go-mode-mark-string-end (point-max)) '(go-mode-string nil)) - (setq go-mode-mark-string-end b))) - (when (<= b go-mode-mark-comment-end) - ;; Remove the property adjacent to the change position. - ;; It may contain positions pointing beyond the new end mark. - (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-comment))) - (if cs (car cs) b)))) - (remove-text-properties - b (min go-mode-mark-string-end (point-max)) '(go-mode-comment nil)) - (setq go-mode-mark-comment-end b))) - - (when (< b go-mode-mark-nesting-end) - (remove-text-properties b (min go-mode-mark-nesting-end (point-max)) '(go-mode-nesting nil)) - (setq go-mode-mark-nesting-end b)))) - -(defmacro go-mode-parser (&rest body) - "Evaluate BODY in an environment set up for parsers that use -text properties to mark text. This inhibits changes to the undo -list or the buffer's modification status and inhibits calls to -the modification hooks. It also saves the excursion and -restriction and widens the buffer, since most parsers are -context-sensitive." - - (let ((modified-var (make-symbol "modified"))) - `(let ((buffer-undo-list t) - (,modified-var (buffer-modified-p)) - (inhibit-modification-hooks t) - (inhibit-read-only t)) - (save-excursion - (save-restriction - (widen) - (unwind-protect - (progn ,@body) - (set-buffer-modified-p ,modified-var))))))) - -(defun go-mode-cs (&optional pos) - "Return the comment/string state at point POS. If point is -inside a comment or string (including the delimiters), this -returns a pair (START . END) indicating the extents of the -comment or string." - - (unless pos - (setq pos (point))) - (when (>= pos go-mode-mark-cs-end) - (go-mode-mark-cs (1+ pos))) - (get-text-property pos 'go-mode-cs)) - -(defun go-mode-mark-cs (end) - "Mark comments and strings up to point END. Don't call this -directly; use `go-mode-cs'." - (setq end (min end (point-max))) - (go-mode-parser - (save-match-data - (let ((pos - ;; Back up to the last known state. - (let ((last-cs - (and (> go-mode-mark-cs-end 1) - (get-text-property (1- go-mode-mark-cs-end) - 'go-mode-cs)))) - (if last-cs - (car last-cs) - (max 1 (1- go-mode-mark-cs-end)))))) - (while (< pos end) - (goto-char pos) - (let ((cs-end ; end of the text property - (cond - ((looking-at "//") - (end-of-line) - (1+ (point))) - ((looking-at "/\\*") - (goto-char (+ pos 2)) - (if (search-forward "*/" (1+ end) t) - (point) - end)) - ((looking-at "\"") - (goto-char (1+ pos)) - (if (looking-at "[^\"\n\\\\]*\\(\\\\.[^\"\n\\\\]*\\)*\"") - (match-end 0) - (end-of-line) - (point))) - ((looking-at "'") - (goto-char (1+ pos)) - (if (looking-at "[^'\n\\\\]*\\(\\\\.[^'\n\\\\]*\\)*'") - (match-end 0) - (end-of-line) - (point))) - ((looking-at "`") - (goto-char (1+ pos)) - (while (if (search-forward "`" end t) - (if (eq (char-after) ?`) - (goto-char (1+ (point)))) - (goto-char end) - nil)) - (point))))) - (cond - (cs-end - (put-text-property pos cs-end 'go-mode-cs (cons pos cs-end)) - (setq pos cs-end)) - ((re-search-forward "[\"'`]\\|/[/*]" end t) - (setq pos (match-beginning 0))) - (t - (setq pos end))))) - (setq go-mode-mark-cs-end pos))))) - -(defun go-mode-in-comment (&optional pos) - "Return the comment/string state at point POS. If point is -inside a comment (including the delimiters), this -returns a pair (START . END) indicating the extents of the -comment or string." - - (unless pos - (setq pos (point))) - (when (> pos go-mode-mark-comment-end) - (go-mode-mark-comment pos)) - (get-text-property pos 'go-mode-comment)) - -(defun go-mode-mark-comment (end) - "Mark comments up to point END. Don't call this directly; use `go-mode-in-comment'." - (setq end (min end (point-max))) - (go-mode-parser - (save-match-data - (let ((pos - ;; Back up to the last known state. - (let ((last-comment - (and (> go-mode-mark-comment-end 1) - (get-text-property (1- go-mode-mark-comment-end) - 'go-mode-comment)))) - (if last-comment - (car last-comment) - (max 1 (1- go-mode-mark-comment-end)))))) - (while (< pos end) - (goto-char pos) - (let ((comment-end ; end of the text property - (cond - ((looking-at "//") - (end-of-line) - (1+ (point))) - ((looking-at "/\\*") - (goto-char (+ pos 2)) - (if (search-forward "*/" (1+ end) t) - (point) - end))))) - (cond - (comment-end - (put-text-property pos comment-end 'go-mode-comment (cons pos comment-end)) - (setq pos comment-end)) - ((re-search-forward "/[/*]" end t) - (setq pos (match-beginning 0))) - (t - (setq pos end))))) - (setq go-mode-mark-comment-end pos))))) - -(defun go-mode-in-string (&optional pos) - "Return the string state at point POS. If point is -inside a string (including the delimiters), this -returns a pair (START . END) indicating the extents of the -comment or string." - - (unless pos - (setq pos (point))) - (when (> pos go-mode-mark-string-end) - (go-mode-mark-string pos)) - (get-text-property pos 'go-mode-string)) - -(defun go-mode-mark-string (end) - "Mark strings up to point END. Don't call this -directly; use `go-mode-in-string'." - (setq end (min end (point-max))) - (go-mode-parser - (save-match-data - (let ((pos - ;; Back up to the last known state. - (let ((last-cs - (and (> go-mode-mark-string-end 1) - (get-text-property (1- go-mode-mark-string-end) - 'go-mode-string)))) - (if last-cs - (car last-cs) - (max 1 (1- go-mode-mark-string-end)))))) - (while (< pos end) - (goto-char pos) - (let ((cs-end ; end of the text property - (cond - ((looking-at "\"") - (goto-char (1+ pos)) - (if (looking-at "[^\"\n\\\\]*\\(\\\\.[^\"\n\\\\]*\\)*\"") - (match-end 0) - (end-of-line) - (point))) - ((looking-at "'") - (goto-char (1+ pos)) - (if (looking-at "[^'\n\\\\]*\\(\\\\.[^'\n\\\\]*\\)*'") - (match-end 0) - (end-of-line) - (point))) - ((looking-at "`") - (goto-char (1+ pos)) - (while (if (search-forward "`" end t) - (if (eq (char-after) ?`) - (goto-char (1+ (point)))) - (goto-char end) - nil)) - (point))))) - (cond - (cs-end - (put-text-property pos cs-end 'go-mode-string (cons pos cs-end)) - (setq pos cs-end)) - ((re-search-forward "[\"'`]" end t) - (setq pos (match-beginning 0))) - (t - (setq pos end))))) - (setq go-mode-mark-string-end pos))))) - -(defun go-mode-font-lock-cs (limit comment) - "Helper function for highlighting comment/strings. If COMMENT is t, -set match data to the next comment after point, and advance point -after it. If COMMENT is nil, use the next string. Returns nil -if no further tokens of the type exist." - ;; Ensures that `next-single-property-change' below will work properly. - (go-mode-cs limit) - (let (cs next (result 'scan)) - (while (eq result 'scan) - (if (or (>= (point) limit) (eobp)) - (setq result nil) - (setq cs (go-mode-cs)) - (if cs - (if (eq (= (char-after (car cs)) ?/) comment) - ;; If inside the expected comment/string, highlight it. - (progn - ;; If the match includes a "\n", we have a - ;; multi-line construct. Mark it as such. - (goto-char (car cs)) - (when (search-forward "\n" (cdr cs) t) - (put-text-property - (car cs) (cdr cs) 'font-lock-multline t)) - (set-match-data (list (car cs) (copy-marker (cdr cs)))) - (goto-char (cdr cs)) - (setq result t)) - ;; Wrong type. Look for next comment/string after this one. - (goto-char (cdr cs))) - ;; Not inside comment/string. Search for next comment/string. - (setq next (next-single-property-change - (point) 'go-mode-cs nil limit)) - (if (and next (< next limit)) - (goto-char next) - (setq result nil))))) - result)) - -(defun go-mode-font-lock-cs-string (limit) - "Font-lock iterator for strings." - (go-mode-font-lock-cs limit nil)) - -(defun go-mode-font-lock-cs-comment (limit) - "Font-lock iterator for comments." - (go-mode-font-lock-cs limit t)) - -(defsubst go-mode-nesting (&optional pos) - "Return the nesting at point POS. The nesting is a list -of (START . END) pairs for all braces, parens, and brackets -surrounding POS, starting at the inner-most nesting. START is -the location of the open character. END is the location of the -close character or nil if the nesting scanner has not yet -encountered the close character." - - (unless pos - (setq pos (point))) - (if (= pos 1) - '() - (when (> pos go-mode-mark-nesting-end) - (go-mode-mark-nesting pos)) - (get-text-property (- pos 1) 'go-mode-nesting))) - -(defun go-mode-mark-nesting (pos) - "Mark nesting up to point END. Don't call this directly; use -`go-mode-nesting'." - - (go-mode-cs pos) - (go-mode-parser - ;; Mark depth - (goto-char go-mode-mark-nesting-end) - (let ((nesting (go-mode-nesting)) - (last (point))) - (while (< last pos) - ;; Find the next depth-changing character - (skip-chars-forward "^(){}[]" pos) - ;; Mark everything up to this character with the current - ;; nesting - (put-text-property last (point) 'go-mode-nesting nesting) - (when nil - (let ((depth (length nesting))) - (put-text-property last (point) 'face - `((:background - ,(format "gray%d" (* depth 10))))))) - (setq last (point)) - ;; Update nesting - (unless (eobp) - (let ((ch (unless (go-mode-cs) (char-after)))) - (forward-char 1) - (case ch - ((?\( ?\{ ?\[) - (setq nesting (cons (cons (- (point) 1) nil) - nesting))) - ((?\) ?\} ?\]) - (when nesting - (setcdr (car nesting) (- (point) 1)) - (setq nesting (cdr nesting)))))))) - ;; Update state - (setq go-mode-mark-nesting-end last)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Indentation -;; - -(defvar go-mode-non-terminating-keywords-regexp - (let* ((kws go-mode-keywords) - (kws (remove "break" kws)) - (kws (remove "continue" kws)) - (kws (remove "fallthrough" kws)) - (kws (remove "return" kws))) - (regexp-opt kws 'words)) - "Regular expression matching all Go keywords that *do not* -implicitly terminate a statement.") - -(defun go-mode-semicolon-p () - "True iff point immediately follows either an explicit or -implicit semicolon. Point should immediately follow the last -token on the line." - - ;; #Semicolons - (case (char-before) - ((?\;) t) - ;; String literal - ((?' ?\" ?`) t) - ;; One of the operators and delimiters ++, --, ), ], or } - ((?+) (eq (char-before (1- (point))) ?+)) - ((?-) (eq (char-before (1- (point))) ?-)) - ((?\) ?\] ?\}) t) - ;; An identifier or one of the keywords break, continue, - ;; fallthrough, or return or a numeric literal - (otherwise - (save-excursion - (when (/= (skip-chars-backward "[:word:]_") 0) - (not (looking-at go-mode-non-terminating-keywords-regexp))))))) - -(defun go-mode-whitespace-p (char) - "Is newline, or char whitespace in the syntax table for go." - (or (eq char ?\n) - (= (char-syntax char) ?\ ))) - -(defun go-mode-backward-skip-comments () - "Skip backward over comments and whitespace." - ;; only proceed if point is in a comment or white space - (if (or (go-mode-in-comment) - (go-mode-whitespace-p (char-after (point)))) - (let ((loop-guard t)) - (while (and - loop-guard - (not (bobp))) - - (cond ((go-mode-whitespace-p (char-after (point))) - ;; moves point back over any whitespace - (re-search-backward "[^[:space:]]")) - - ((go-mode-in-comment) - ;; move point to char preceeding current comment - (goto-char (1- (car (go-mode-in-comment))))) - - ;; not in a comment or whitespace? we must be done. - (t (setq loop-guard nil) - (forward-char 1))))))) - -(defun go-mode-indentation () - "Compute the ideal indentation level of the current line. - -To the first order, this is the brace depth of the current line, -plus parens that follow certain keywords. case, default, and -labels are outdented one level, and continuation lines are -indented one level." - +(defmacro go-paren-level () + `(car (syntax-ppss))) + +(defmacro go-in-string-or-comment-p () + `(nth 8 (syntax-ppss))) + +(defmacro go-in-string-p () + `(nth 3 (syntax-ppss))) + +(defmacro go-in-comment-p () + `(nth 4 (syntax-ppss))) + +(defmacro go-goto-beginning-of-string-or-comment () + `(goto-char (nth 8 (syntax-ppss)))) + +(defun go--backward-irrelevant (&optional stop-at-string) + "Skips backwards over any characters that are irrelevant for +indentation and related tasks. + +It skips over whitespace, comments, cases and labels and, if +STOP-AT-STRING is not true, over strings." + + (let (pos (start-pos (point))) + (skip-chars-backward "\n[:blank:]") + (if (and (save-excursion (beginning-of-line) (go-in-string-p)) (looking-back "`") (not stop-at-string)) + (backward-char)) + (if (and (go-in-string-p) (not stop-at-string)) + (go-goto-beginning-of-string-or-comment)) + (if (looking-back "\\*/") + (backward-char)) + (if (go-in-comment-p) + (go-goto-beginning-of-string-or-comment)) + (setq pos (point)) + (beginning-of-line) + (if (or (looking-at (concat "^" go-label-regexp ":")) (looking-at "^[[:space:]]*\\(case .+\\|default\\):")) + (end-of-line 0) + (goto-char pos)) + (if (/= start-pos (point)) + (go--backward-irrelevant stop-at-string)) + (/= start-pos (point)))) + +(defun go--buffer-narrowed-p () + "Return non-nil if the current buffer is narrowed." + (/= (buffer-size) + (- (point-max) + (point-min)))) + +(defun go-previous-line-has-dangling-op-p () + "Returns non-nil if the current line is a continuation line." + (let* ((cur-line (line-number-at-pos)) + (val (gethash cur-line go-dangling-cache 'nope))) + (if (or (go--buffer-narrowed-p) (equal val 'nope)) + (save-excursion + (beginning-of-line) + (go--backward-irrelevant t) + (setq val (looking-back go-dangling-operators-regexp)) + (if (not (go--buffer-narrowed-p)) + (puthash cur-line val go-dangling-cache)))) + val)) + +(defun go-goto-opening-parenthesis (&optional char) + (let ((start-nesting (go-paren-level))) + (while (and (not (bobp)) + (>= (go-paren-level) start-nesting)) + (if (zerop (skip-chars-backward + (if char + (case char (?\] "^[") (?\} "^{") (?\) "^(")) + "^[{("))) + (if (go-in-string-or-comment-p) + (go-goto-beginning-of-string-or-comment) + (backward-char)))))) + +(defun go-indentation-at-point () (save-excursion - (back-to-indentation) - (let ((cs (go-mode-cs))) - ;; Treat comments and strings differently only if the beginning - ;; of the line is contained within them - (when (and cs (= (point) (car cs))) - (setq cs nil)) - ;; What type of context am I in? + (let (start-nesting (outindent 0)) + (back-to-indentation) + (setq start-nesting (go-paren-level)) + (cond - ((and cs (save-excursion - (goto-char (car cs)) - (looking-at "`"))) - ;; Inside a multi-line string. Don't mess with indentation. - nil) - (cs - ;; Inside a general comment - (goto-char (car cs)) - (forward-char 1) - (current-column)) + ((go-in-string-p) + (current-indentation)) + ((looking-at "[])}]") + (go-goto-opening-parenthesis (char-after)) + (if (go-previous-line-has-dangling-op-p) + (- (current-indentation) tab-width) + (current-indentation))) + ((progn (go--backward-irrelevant t) (looking-back go-dangling-operators-regexp)) + ;; only one nesting for all dangling operators in one operation + (if (go-previous-line-has-dangling-op-p) + (current-indentation) + (+ (current-indentation) tab-width))) + ((zerop (go-paren-level)) + 0) + ((progn (go-goto-opening-parenthesis) (< (go-paren-level) start-nesting)) + (if (go-previous-line-has-dangling-op-p) + (current-indentation) + (+ (current-indentation) tab-width))) (t - ;; Not in a multi-line string or comment - (let ((indent 0) - (inside-indenting-paren nil)) - ;; Count every enclosing brace, plus parens that follow - ;; import, const, var, or type and indent according to - ;; depth. This simple rule does quite well, but also has a - ;; very large extent. It would be better if we could mimic - ;; some nearby indentation. - (save-excursion - (skip-chars-forward "})") - (let ((first t)) - (dolist (nest (go-mode-nesting)) - (case (char-after (car nest)) - ((?\{) - (incf indent tab-width)) - ((?\() - (goto-char (car nest)) - (go-mode-backward-skip-comments) - (backward-char) - ;; Really just want the token before - (when (looking-back "\\<import\\|const\\|var\\|type\\|package" - (max (- (point) 7) (point-min))) - (incf indent tab-width) - (when first - (setq inside-indenting-paren t))))) - (setq first nil)))) - - ;; case, default, and labels are outdented 1 level - (when (looking-at "\\<case\\>\\|\\<default\\>\\|\\w+\\s *:\\(\\S.\\|$\\)") - (decf indent tab-width)) - - (when (looking-at "\\w+\\s *:.+,\\s *$") - (incf indent tab-width)) - - ;; Continuation lines are indented 1 level - (beginning-of-line) ; back up to end of previous line - (backward-char) - (go-mode-backward-skip-comments) ; back up past any comments - (when (case (char-before) - ((nil ?\{ ?:) - ;; At the beginning of a block or the statement - ;; following a label. - nil) - ((?\() - ;; Usually a continuation line in an expression, - ;; unless this paren is part of a factored - ;; declaration. - (not inside-indenting-paren)) - ((?,) - ;; Could be inside a literal. We're a little - ;; conservative here and consider any comma within - ;; curly braces (as opposed to parens) to be a - ;; literal separator. This will fail to recognize - ;; line-breaks in parallel assignments as - ;; continuation lines. - (let ((depth (go-mode-nesting))) - (and depth - (not (eq (char-after (caar depth)) ?\{))))) - (t - ;; We're in the middle of a block. Did the - ;; previous line end with an implicit or explicit - ;; semicolon? - (not (go-mode-semicolon-p)))) - (incf indent tab-width)) - - (max indent 0))))))) + (current-indentation)))))) (defun go-mode-indent-line () - "Indent the current line according to `go-mode-indentation'." (interactive) - - ;; turn off case folding to distinguish keywords from identifiers - ;; e.g. "default" is a keyword; "Default" can be a variable name. - (let ((case-fold-search nil)) - (let ((col (go-mode-indentation))) - (when col - (let ((offset (- (current-column) (current-indentation)))) - (indent-line-to col) - (when (> offset 0) - (forward-char offset))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Go mode -;; + (let (indent + shift-amt + end + (pos (- (point-max) (point))) + (point (point)) + (beg (line-beginning-position))) + (back-to-indentation) + (if (go-in-string-or-comment-p) + (goto-char point) + (setq indent (go-indentation-at-point)) + (if (looking-at (concat go-label-regexp ":\\([[:space:]]*/.+\\)?$\\|case .+:\\|default:")) + (decf indent tab-width)) + (setq shift-amt (- indent (current-column))) + (if (zerop shift-amt) + nil + (delete-region beg (point)) + (indent-to indent)) + ;; If initial point was within line's indentation, + ;; position after the indentation. Else stay at same point in text. + (if (> (- (point-max) pos) (point)) + (goto-char (- (point-max) pos)))))) + +(defun go-beginning-of-defun (&optional count) + (unless count (setq count 1)) + (let ((first t) failure) + (dotimes (i (abs count)) + (while (and (not failure) + (or first (go-in-string-or-comment-p))) + (if (>= count 0) + (progn + (go--backward-irrelevant) + (if (not (re-search-backward go-func-meth-regexp nil t)) + (setq failure t))) + (if (looking-at go-func-meth-regexp) + (forward-char)) + (if (not (re-search-forward go-func-meth-regexp nil t)) + (setq failure t))) + (setq first nil))) + (if (< count 0) + (beginning-of-line)) + (not failure))) + +(defun go-end-of-defun () + (let (orig-level) + ;; It can happen that we're not placed before a function by emacs + (if (not (looking-at "func")) + (go-beginning-of-defun -1)) + (skip-chars-forward "^{") + (forward-char) + (setq orig-level (go-paren-level)) + (while (>= (go-paren-level) orig-level) + (skip-chars-forward "^}") + (forward-char)))) ;;;###autoload -(define-derived-mode go-mode nil "Go" +(define-derived-mode go-mode fundamental-mode "Go" "Major mode for editing Go source text. -This provides basic syntax highlighting for keywords, built-ins, -functions, and some types. It also provides indentation that is -\(almost) identical to gofmt." +This mode provides (not just) basic editing capabilities for +working with Go code. It offers almost complete syntax +highlighting, indentation that is almost identical to gofmt and +proper parsing of the buffer content to allow features such as +navigation by function, manipulation of comments or detection of +strings. + +In addition to these core features, it offers various features to +help with writing Go code. You can directly run buffer content +through gofmt, read godoc documentation from within Emacs, modify +and clean up the list of package imports or interact with the +Playground (uploading and downloading pastes). + +The following extra functions are defined: + +- `gofmt' +- `godoc' +- `go-import-add' +- `go-remove-unused-imports' +- `go-goto-imports' +- `go-play-buffer' and `go-play-region' +- `go-download-play' + +If you want to automatically run `gofmt' before saving a file, +add the following hook to your emacs configuration: + +\(add-hook 'before-save-hook 'gofmt-before-save) + +If you're looking for even more integration with Go, namely +on-the-fly syntax checking, auto-completion and snippets, it is +recommended that you look at goflymake +\(https://github.com/dougm/goflymake), gocode +\(https://github.com/nsf/gocode) and yasnippet-go +\(https://github.com/dominikh/yasnippet-go)" ;; Font lock (set (make-local-variable 'font-lock-defaults) - '(go-mode-font-lock-keywords nil nil nil nil)) - - ;; Remove stale text properties - (save-restriction - (widen) - (let ((modified (buffer-modified-p))) - (remove-text-properties 1 (point-max) - '(go-mode-cs nil go-mode-nesting nil)) - ;; remove-text-properties marks the buffer modified. undo that if it - ;; wasn't originally marked modified. - (set-buffer-modified-p modified))) - - ;; Reset the syntax mark caches - (setq go-mode-mark-cs-end 1 - go-mode-mark-nesting-end 1) - (add-hook 'before-change-functions #'go-mode-mark-clear-cache nil t) - (add-hook 'after-change-functions #'go-mode-mark-clear-cs nil t) + '(go--build-font-lock-keywords)) ;; Indentation - (set (make-local-variable 'indent-line-function) - #'go-mode-indent-line) - (add-hook 'after-change-functions #'go-mode-delayed-electric-hook nil t) + (set (make-local-variable 'indent-line-function) 'go-mode-indent-line) ;; Comments (set (make-local-variable 'comment-start) "// ") (set (make-local-variable 'comment-end) "") + (set (make-local-variable 'comment-use-syntax) t) + (set (make-local-variable 'comment-start-skip) "\\(//+\\|/\\*+\\)\\s *") + + (set (make-local-variable 'beginning-of-defun-function) 'go-beginning-of-defun) + (set (make-local-variable 'end-of-defun-function) 'go-end-of-defun) + + (set (make-local-variable 'parse-sexp-lookup-properties) t) + (if (boundp 'syntax-propertize-function) + (set (make-local-variable 'syntax-propertize-function) 'go-propertize-syntax)) + + (set (make-local-variable 'go-dangling-cache) (make-hash-table :test 'eql)) + (add-hook 'before-change-functions (lambda (x y) (setq go-dangling-cache (make-hash-table :test 'eql))) t t) + + + (setq imenu-generic-expression + '(("type" "^type *\\([^ \t\n\r\f]*\\)" 1) + ("func" "^func *\\(.*\\) {" 1))) + (imenu-add-to-menubar "Index") ;; Go style (setq indent-tabs-mode t) @@ -739,24 +352,14 @@ functions, and some types. It also provides indentation that is ;; those alists are traversed in *reverse* order: ;; http://lists.gnu.org/archive/html/bug-gnu-emacs/2001-12/msg00674.html (when (and (boundp 'compilation-error-regexp-alist) - (boundp 'compilation-error-regexp-alist-alist)) - (add-to-list 'compilation-error-regexp-alist 'go-test t) - (add-to-list 'compilation-error-regexp-alist-alist - '(go-test . ("^\t+\\([^()\t\n]+\\):\\([0-9]+\\):? .*$" 1 2)) t))) + (boundp 'compilation-error-regexp-alist-alist)) + (add-to-list 'compilation-error-regexp-alist 'go-test t) + (add-to-list 'compilation-error-regexp-alist-alist + '(go-test . ("^\t+\\([^()\t\n]+\\):\\([0-9]+\\):? .*$" 1 2)) t))) ;;;###autoload -(add-to-list 'auto-mode-alist (cons "\\.go$" #'go-mode)) - -(defun go-mode-reload () - "Reload go-mode.el and put the current buffer into Go mode. -Useful for development work." +(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode)) - (interactive) - (unload-feature 'go-mode) - (require 'go-mode) - (go-mode)) - -;;;###autoload (defun gofmt () "Pipe the current buffer through the external tool `gofmt`. Replace the current buffer on success; display errors on failure." @@ -768,84 +371,103 @@ Replace the current buffer on success; display errors on failure." (patchbuf (get-buffer-create "*Gofmt patch*"))) (with-current-buffer patchbuf (let ((errbuf (get-buffer-create "*Gofmt Errors*")) - (coding-system-for-read 'utf-8) ;; use utf-8 with subprocesses + ;; use utf-8 with subprocesses + (coding-system-for-read 'utf-8) (coding-system-for-write 'utf-8)) (with-current-buffer errbuf - (toggle-read-only 0) + (setq buffer-read-only nil) (erase-buffer)) (with-current-buffer srcbuf (save-restriction (let (deactivate-mark) (widen) - ; If this is a new file, diff-mode can't apply a - ; patch to a non-exisiting file, so replace the buffer - ; completely with the output of 'gofmt'. - ; If the file exists, patch it to keep the 'undo' list happy. + ;; If this is a new file, diff-mode can't apply a + ;; patch to a non-exisiting file, so replace the buffer + ;; completely with the output of 'gofmt'. + ;; If the file exists, patch it to keep the 'undo' list happy. (let* ((newfile (not (file-exists-p filename))) - (flag (if newfile "" " -d"))) - (if (= 0 (shell-command-on-region (point-min) (point-max) - (concat "gofmt" flag) - patchbuf nil errbuf)) - ; gofmt succeeded: replace buffer or apply patch hunks. + (flag (if newfile "" " -d"))) + + ;; diff-mode doesn't work too well with missing + ;; end-of-file newline, so add one + (if (/= (char-after (1- (point-max))) ?\n) + (save-excursion + (goto-char (point-max)) + (insert ?\n))) + + (if (zerop (shell-command-on-region (point-min) (point-max) + (concat "gofmt" flag) + patchbuf nil errbuf)) + ;; gofmt succeeded: replace buffer or apply patch hunks. (let ((old-point (point)) (old-mark (mark t))) (kill-buffer errbuf) (if newfile - ; New file, replace it (diff-mode won't work) - (gofmt-replace-buffer srcbuf patchbuf) - ; Existing file, patch it - (gofmt-apply-patch filename srcbuf patchbuf)) + ;; New file, replace it (diff-mode won't work) + (gofmt--replace-buffer srcbuf patchbuf) + ;; Existing file, patch it + (gofmt--apply-patch filename srcbuf patchbuf)) (goto-char (min old-point (point-max))) ;; Restore the mark and point (if old-mark (push-mark (min old-mark (point-max)) t)) (set-window-configuration currconf)) - ;; gofmt failed: display the errors - (gofmt-process-errors filename errbuf)))))) + ;; gofmt failed: display the errors + (message "Could not apply gofmt. Check errors for details") + (gofmt--process-errors filename errbuf)))))) ;; Collapse any window opened on outbuf if shell-command-on-region ;; displayed it. (delete-windows-on patchbuf))) (kill-buffer patchbuf)))) -(defun gofmt-replace-buffer (srcbuf patchbuf) +(defun gofmt--replace-buffer (srcbuf patchbuf) (with-current-buffer srcbuf (erase-buffer) - (insert-buffer-substring patchbuf))) + (insert-buffer-substring patchbuf)) + (message "Applied gofmt")) -(defconst gofmt-stdin-tag "<standard input>") - -(defun gofmt-apply-patch (filename srcbuf patchbuf) - (require 'diff-mode) +(defun gofmt--apply-patch (filename srcbuf patchbuf) ;; apply all the patch hunks - (with-current-buffer patchbuf - (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename) - nil (point-min) (point-max)) - (condition-case nil - (while t - (diff-hunk-next) - (diff-apply-hunk)) - ;; When there's no more hunks, diff-hunk-next signals an error, ignore it - (error nil)))) - -(defun gofmt-process-errors (filename errbuf) + (let (changed) + (with-current-buffer patchbuf + (goto-char (point-min)) + ;; The .* is for TMPDIR, but to avoid dealing with TMPDIR + ;; having a trailing / or not, it's easier to just search for .* + ;; especially as we're only replacing the first instance. + (if (re-search-forward "^--- \\(.*/gofmt[0-9]*\\)" nil t) + (replace-match filename nil nil nil 1)) + (condition-case nil + (while t + (diff-hunk-next) + (diff-apply-hunk) + (setq changed t)) + ;; When there's no more hunks, diff-hunk-next signals an error, ignore it + (error nil))) + (if changed (message "Applied gofmt") (message "Buffer was already gofmted")))) + +(defun gofmt--process-errors (filename errbuf) ;; Convert the gofmt stderr to something understood by the compilation mode. (with-current-buffer errbuf - (beginning-of-buffer) + (goto-char (point-min)) (insert "gofmt errors:\n") - (replace-string gofmt-stdin-tag (file-name-nondirectory filename) nil (point-min) (point-max)) + (if (search-forward gofmt-stdin-tag nil t) + (replace-match (file-name-nondirectory filename) nil t)) (display-buffer errbuf) (compilation-mode))) ;;;###autoload (defun gofmt-before-save () "Add this to .emacs to run gofmt on the current buffer when saving: - (add-hook 'before-save-hook #'gofmt-before-save)" + (add-hook 'before-save-hook 'gofmt-before-save). + +Note that this will cause go-mode to get loaded the first time +you save any file, kind of defeating the point of autoloading." (interactive) (when (eq major-mode 'go-mode) (gofmt))) -(defun godoc-read-query () +(defun godoc--read-query () "Read a godoc query from the minibuffer." ;; Compute the default query as the symbol under the cursor. ;; TODO: This does the wrong thing for e.g. multipart.NewReader (it only grabs @@ -859,7 +481,7 @@ Replace the current buffer on success; display errors on failure." "godoc: ") nil nil symbol))) -(defun godoc-get-buffer (query) +(defun godoc--get-buffer (query) "Get an empty buffer for a godoc query." (let* ((buffer-name (concat "*godoc " query "*")) (buffer (get-buffer buffer-name))) @@ -867,13 +489,14 @@ Replace the current buffer on success; display errors on failure." (when buffer (kill-buffer buffer)) (get-buffer-create buffer-name))) -(defun godoc-buffer-sentinel (proc event) +(defun godoc--buffer-sentinel (proc event) "Sentinel function run when godoc command completes." (with-current-buffer (process-buffer proc) (cond ((string= event "finished\n") ;; Successful exit. (goto-char (point-min)) - (display-buffer (current-buffer) 'not-this-window)) - ((not (= (process-exit-status proc) 0)) ;; Error exit. + (view-mode 1) + (display-buffer (current-buffer) t)) + ((/= (process-exit-status proc) 0) ;; Error exit. (let ((output (buffer-string))) (kill-buffer (current-buffer)) (message (concat "godoc: " output))))))) @@ -881,12 +504,217 @@ Replace the current buffer on success; display errors on failure." ;;;###autoload (defun godoc (query) "Show go documentation for a query, much like M-x man." - (interactive (list (godoc-read-query))) + (interactive (list (godoc--read-query))) (unless (string= query "") (set-process-sentinel - (start-process-shell-command "godoc" (godoc-get-buffer query) + (start-process-shell-command "godoc" (godoc--get-buffer query) (concat "godoc " query)) - 'godoc-buffer-sentinel) + 'godoc--buffer-sentinel) nil)) +(defun go-goto-imports () + "Move point to the block of imports. + +If using + + import ( + \"foo\" + \"bar\" + ) + +it will move point directly behind the last import. + +If using + + import \"foo\" + import \"bar\" + +it will move point to the next line after the last import. + +If no imports can be found, point will be moved after the package +declaration." + (interactive) + ;; FIXME if there's a block-commented import before the real + ;; imports, we'll jump to that one. + + ;; Generally, this function isn't very forgiving. it'll bark on + ;; extra whitespace. It works well for clean code. + (let ((old-point (point))) + (goto-char (point-min)) + (cond + ((re-search-forward "^import ([^)]+)" nil t) + (backward-char 2) + 'block) + ((re-search-forward "\\(^import \\([^\"]+ \\)?\"[^\"]+\"\n?\\)+" nil t) + 'single) + ((re-search-forward "^[[:space:]\n]*package .+?\n" nil t) + (message "No imports found, moving point after package declaration") + 'none) + (t + (goto-char old-point) + (message "No imports or package declaration found. Is this really a Go file?") + 'fail)))) + +(defun go-play-buffer () + "Like `go-play-region', but acts on the entire buffer." + (interactive) + (go-play-region (point-min) (point-max))) + +(defun go-play-region (start end) + "Send the region to the Playground and stores the resulting +link in the kill ring." + (interactive "r") + (let* ((url-request-method "POST") + (url-request-extra-headers + '(("Content-Type" . "application/x-www-form-urlencoded"))) + (url-request-data (buffer-substring-no-properties start end)) + (content-buf (url-retrieve + "http://play.golang.org/share" + (lambda (arg) + (cond + ((equal :error (car arg)) + (signal 'go-play-error (cdr arg))) + (t + (re-search-forward "\n\n") + (kill-new (format "http://play.golang.org/p/%s" (buffer-substring (point) (point-max)))) + (message "http://play.golang.org/p/%s" (buffer-substring (point) (point-max))))))))))) + +;;;###autoload +(defun go-download-play (url) + "Downloads a paste from the playground and inserts it in a Go +buffer. Tries to look for a URL at point." + (interactive (list (read-from-minibuffer "Playground URL: " (ffap-url-p (ffap-string-at-point 'url))))) + (with-current-buffer + (let ((url-request-method "GET") url-request-data url-request-extra-headers) + (url-retrieve-synchronously (concat url ".go"))) + (let ((buffer (generate-new-buffer (concat (car (last (split-string url "/"))) ".go")))) + (goto-char (point-min)) + (re-search-forward "\n\n") + (copy-to-buffer buffer (point) (point-max)) + (kill-buffer) + (with-current-buffer buffer + (go-mode) + (switch-to-buffer buffer))))) + +(defun go-propertize-syntax (start end) + (save-excursion + (goto-char start) + (while (search-forward "\\" end t) + (put-text-property (1- (point)) (point) 'syntax-table (if (= (char-after) ?`) '(1) '(9)))))) + +;; ;; Commented until we actually make use of this function +;; (defun go--common-prefix (sequences) +;; ;; mismatch and reduce are cl +;; (assert sequences) +;; (flet ((common-prefix (s1 s2) +;; (let ((diff-pos (mismatch s1 s2))) +;; (if diff-pos (subseq s1 0 diff-pos) s1)))) +;; (reduce #'common-prefix sequences))) + +(defun go-import-add (arg import) + "Add a new import to the list of imports. + +When called with a prefix argument asks for an alternative name +to import the package as. + +If no list exists yet, one will be created if possible. + +If an identical import has been commented, it will be +uncommented, otherwise a new import will be added." + + ;; - If there's a matching `// import "foo"`, uncomment it + ;; - If we're in an import() block and there's a matching `"foo"`, uncomment it + ;; - Otherwise add a new import, with the appropriate syntax + (interactive + (list + current-prefix-arg + (replace-regexp-in-string "^[\"']\\|[\"']$" "" (completing-read "Package: " (go-packages))))) + (save-excursion + (let (as line import-start) + (if arg + (setq as (read-from-minibuffer "Import as: "))) + (if as + (setq line (format "%s \"%s\"" as import)) + (setq line (format "\"%s\"" import))) + + (goto-char (point-min)) + (if (re-search-forward (concat "^[[:space:]]*//[[:space:]]*import " line "$") nil t) + (uncomment-region (line-beginning-position) (line-end-position)) + (case (go-goto-imports) + ('fail (message "Could not find a place to add import.")) + ('block + (save-excursion + (re-search-backward "^import (") + (setq import-start (point))) + (if (re-search-backward (concat "^[[:space:]]*//[[:space:]]*" line "$") import-start t) + (uncomment-region (line-beginning-position) (line-end-position)) + (insert "\n\t" line))) + ('single (insert "import " line "\n")) + ('none (insert "\nimport (\n\t" line "\n)\n"))))))) + +(defun go-root-and-paths () + (let* ((output (process-lines "go" "env" "GOROOT" "GOPATH")) + (root (car output)) + (paths (split-string (car (cdr output)) ":"))) + (append (list root) paths))) + +(defun go-packages () + (sort + (delete-dups + (mapcan + (lambda (topdir) + (let ((pkgdir (concat topdir "/pkg/"))) + (mapcan (lambda (dir) + (mapcar (lambda (file) + (let ((sub (substring file (length pkgdir) -2))) + (unless (or (string-prefix-p "obj/" sub) (string-prefix-p "tool/" sub)) + (mapconcat 'identity (cdr (split-string sub "/")) "/")))) + (if (file-directory-p dir) + (directory-files dir t "\\.a$")))) + (if (file-directory-p pkgdir) + (find-lisp-find-files-internal pkgdir 'find-lisp-file-predicate-is-directory 'find-lisp-default-directory-predicate))))) + (go-root-and-paths))) + 'string<)) + +(defun go-unused-imports-lines () + ;; FIXME Technically, -o /dev/null fails in quite some cases (on + ;; Windows, when compiling from within GOPATH). Practically, + ;; however, it has the same end result: There won't be a + ;; compiled binary/archive, and we'll get our import errors when + ;; there are any. + (reverse (remove nil + (mapcar + (lambda (line) + (if (string-match "^\\(.+\\):\\([[:digit:]]+\\): imported and not used: \".+\"$" line) + (if (string= (file-truename (match-string 1 line)) (file-truename buffer-file-name)) + (string-to-number (match-string 2 line))))) + (split-string (shell-command-to-string + (if (string-match "_test\.go$" buffer-file-truename) + "go test -c" + "go build -o /dev/null")) "\n"))))) + +(defun go-remove-unused-imports (arg) + "Removes all unused imports. If ARG is non-nil, unused imports +will be commented, otherwise they will be removed completely." + (interactive "P") + (save-excursion + (let ((cur-buffer (current-buffer)) flymake-state lines) + (when (boundp 'flymake-mode) + (setq flymake-state flymake-mode) + (flymake-mode-off)) + (save-some-buffers nil (lambda () (equal cur-buffer (current-buffer)))) + (if (buffer-modified-p) + (message "Cannot operate on unsaved buffer") + (setq lines (go-unused-imports-lines)) + (dolist (import lines) + (goto-char (point-min)) + (forward-line (1- import)) + (beginning-of-line) + (if arg + (comment-region (line-beginning-position) (line-end-position)) + (let ((kill-whole-line t)) + (kill-line)))) + (message "Removed %d imports" (length lines))) + (if flymake-state (flymake-mode-on))))) + (provide 'go-mode) diff --git a/misc/git/pre-commit b/misc/git/pre-commit new file mode 100755 index 000000000..18b7f832f --- /dev/null +++ b/misc/git/pre-commit @@ -0,0 +1,26 @@ +#!/bin/sh +# 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. + +# git gofmt pre-commit hook +# +# To use, store as .git/hooks/pre-commit inside your repository and make sure +# it has execute permissions. +# +# This script does not handle file names that contain spaces. + +gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$') +[ -z "$gofiles" ] && exit 0 + +unformatted=$(gofmt -l $gofiles) +[ -z "$unformatted" ] && exit 0 + +# Some files are not gofmt'd. Print message and fail. + +echo >&2 "Go files must be formatted with gofmt. Please run:" +for fn in $unformatted; do + echo >&2 " gofmt -w $PWD/$fn" +done + +exit 1 diff --git a/misc/goplay/doc.go b/misc/goplay/doc.go index fd4a28e71..61e74a000 100644 --- a/misc/goplay/doc.go +++ b/misc/goplay/doc.go @@ -4,20 +4,20 @@ // Goplay is a web interface for experimenting with Go code. // It is similar to the Go Playground: http://golang.org/doc/play/ -// +// // To use goplay: // $ cd $GOROOT/misc/goplay // $ go run goplay.go // and load http://localhost:3999/ in a web browser. -// +// // You should see a Hello World program, which you can compile and run by // pressing shift-enter. There is also a "compile-on-keypress" feature that can // be enabled by checking a checkbox. -// +// // WARNING! CUIDADO! ACHTUNG! ATTENZIONE! // A note on security: anyone with access to the goplay web interface can run // arbitrary code on your computer. Goplay is not a sandbox, and has no other -// security mechanisms. Do not deploy it in untrusted environments. -// By default, goplay listens only on localhost. This can be overridden with +// security mechanisms. Do not deploy it in untrusted environments. +// By default, goplay listens only on localhost. This can be overridden with // the -http parameter. Do so at your own risk. -package documentation +package main diff --git a/misc/goplay/goplay.go b/misc/goplay/goplay.go index 9ce4f89ae..94d04139d 100644 --- a/misc/goplay/goplay.go +++ b/misc/goplay/goplay.go @@ -44,7 +44,7 @@ func main() { log.Fatal(http.ListenAndServe(*httpListen, nil)) } -// FrontPage is an HTTP handler that renders the goplay interface. +// FrontPage is an HTTP handler that renders the goplay interface. // If a filename is supplied in the path component of the URI, // its contents will be put in the interface's text area. // Otherwise, the default "hello, world" program is displayed. diff --git a/misc/osx/README b/misc/osx/README deleted file mode 100644 index 2408dc459..000000000 --- a/misc/osx/README +++ /dev/null @@ -1,3 +0,0 @@ -Use package.bash to construct a package file (Go.pkg) for installation on OS X. - -This script depends on PackageMaker (Developer Tools). diff --git a/misc/osx/etc/paths.d/go b/misc/osx/etc/paths.d/go deleted file mode 100644 index 532e5f936..000000000 --- a/misc/osx/etc/paths.d/go +++ /dev/null @@ -1 +0,0 @@ -/usr/local/go/bin diff --git a/misc/osx/package.bash b/misc/osx/package.bash deleted file mode 100755 index d4ee5f48f..000000000 --- a/misc/osx/package.bash +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Copyright 2011 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. - -set -e - -if ! test -f ../../src/all.bash; then - echo >&2 "package.bash must be run from $GOROOT/misc/osx" - exit 1 -fi - -echo >&2 "Locating PackageMaker..." -PM=/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker -if [ ! -x $PM ]; then - PM=/Developer$PM - if [ ! -x $PM ]; then - echo >&2 "could not find PackageMaker; aborting" - exit 1 - fi -fi -echo >&2 " Found: $PM" - -BUILD=/tmp/go.build.tmp -ROOT=`hg root` -export GOROOT=$BUILD/root/usr/local/go -export GOROOT_FINAL=/usr/local/go - -echo >&2 "Removing old images" -rm -f *.pkg *.dmg - -echo >&2 "Preparing temporary directory" -rm -rf $BUILD -mkdir -p $BUILD -trap "rm -rf $BUILD" 0 - -echo >&2 "Copying go source distribution" -mkdir -p $BUILD/root/usr/local -cp -r $ROOT $GOROOT -cp -r etc $BUILD/root/etc - -pushd $GOROOT > /dev/null - -echo >&2 "Detecting version..." -pushd src > /dev/null -./make.bash --dist-tool > /dev/null -../bin/tool/dist version > /dev/null -popd > /dev/null -mv VERSION.cache VERSION -VERSION="$(cat VERSION | awk '{ print $1 }')" -echo >&2 " Version: $VERSION" - -echo >&2 "Pruning Mercurial metadata" -rm -rf .hg .hgignore .hgtags - -echo >&2 "Building Go" -pushd src -./all.bash 2>&1 | sed "s/^/ /" >&2 -popd > /dev/null - -popd > /dev/null - -echo >&2 "Building package" -$PM -v -r $BUILD/root -o "go.darwin.$VERSION.pkg" \ - --scripts scripts \ - --id com.googlecode.go \ - --title Go \ - --version "0.1" \ - --target "10.5" diff --git a/misc/osx/scripts/postinstall b/misc/osx/scripts/postinstall deleted file mode 100644 index 3748721c7..000000000 --- a/misc/osx/scripts/postinstall +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -GOROOT=/usr/local/go - -echo "Fixing permissions" -cd $GOROOT -find . -exec chmod ugo+r \{\} \; -find bin -exec chmod ugo+rx \{\} \; -find . -type d -exec chmod ugo+rx \{\} \; -chmod o-w . - -echo "Fixing debuggers via sudo.bash" -# setgrp procmod the debuggers (sudo.bash) -cd $GOROOT/src -./sudo.bash - -echo "Installing miscellaneous files:" -XCODE_MISC_DIR="/Library/Application Support/Developer/Shared/Xcode/Specifications/" -if [ -f $XCODE_MISC_DIR ]; then - echo " XCode" - cp $GOROOT/misc/xcode/* $XCODE_MISC_DIR -fi - diff --git a/misc/pprof b/misc/pprof index 2fe56503c..7c379acbe 100755 --- a/misc/pprof +++ b/misc/pprof @@ -634,7 +634,7 @@ sub Main() { # (only matters when --heapcheck is given but we must be # compatible with old branches that did not pass --heapcheck always): if ($total != 0) { - printf("Total: %s %s\n", Unparse($total), Units()); + Infof("Total: %s %s\n", Unparse($total), Units()); } PrintText($symbols, $flat, $cumulative, $total, -1); } elsif ($main::opt_raw) { @@ -726,10 +726,8 @@ sub RunWeb { "firefox", ); foreach my $b (@alt) { - if (-f $b) { - if (system($b, $fname) == 0) { - return; - } + if (system($b, $fname) == 0) { + return; } } @@ -931,7 +929,7 @@ sub ProcessProfile { if ($focus ne '') { $profile = FocusProfile($symbols, $profile, $focus); my $focus_count = TotalProfile($profile); - printf("After focusing on '%s': %s %s of %s (%0.1f%%)\n", + Infof("After focusing on '%s': %s %s of %s (%0.1f%%)\n", $focus, Unparse($focus_count), Units(), Unparse($total_count), ($focus_count*100.0) / $total_count); @@ -939,7 +937,7 @@ sub ProcessProfile { if ($ignore ne '') { $profile = IgnoreProfile($symbols, $profile, $ignore); my $ignore_count = TotalProfile($profile); - printf("After ignoring '%s': %s %s of %s (%0.1f%%)\n", + Infof("After ignoring '%s': %s %s of %s (%0.1f%%)\n", $ignore, Unparse($ignore_count), Units(), Unparse($total_count), @@ -1115,6 +1113,15 @@ sub PrintSymbolizedProfile { } } +# Print information conditionally filtered out depending on the output +# format. +sub Infof { + my $format = shift; + my @args = @_; + return if $main::opt_svg; + printf($format, @args); +} + # Print text output sub PrintText { my $symbols = shift; @@ -2638,6 +2645,8 @@ sub RemoveUninterestingFrames { 'runtime.makemap_c', 'runtime.makeslice', 'runtime.mal', + 'runtime.settype', + 'runtime.settype_flush', 'runtime.slicebytetostring', 'runtime.sliceinttostring', 'runtime.stringtoslicebyte', @@ -2971,32 +2980,32 @@ print STDERR "Read $url\n"; sub IsProfileURL { my $profile_name = shift; - my ($host, $port, $prefix, $path) = ParseProfileURL($profile_name); + my ($scheme, $host, $port, $prefix, $path) = ParseProfileURL($profile_name); return defined($host) and defined($port) and defined($path); } sub ParseProfileURL { my $profile_name = shift; if (defined($profile_name) && - $profile_name =~ m,^(http://|)([^/:]+):(\d+)(|\@\d+)(|/|(.*?)($PROFILE_PAGE|$PMUPROFILE_PAGE|$HEAP_PAGE|$GROWTH_PAGE|$THREAD_PAGE|$CONTENTION_PAGE|$WALL_PAGE|$FILTEREDPROFILE_PAGE))$,o) { + $profile_name =~ m,^(?:(https?)://|)([^/:]+):(\d+)(|\@\d+)(|/|(.*?)($PROFILE_PAGE|$PMUPROFILE_PAGE|$HEAP_PAGE|$GROWTH_PAGE|$THREAD_PAGE|$CONTENTION_PAGE|$WALL_PAGE|$FILTEREDPROFILE_PAGE))$,o) { # $7 is $PROFILE_PAGE/$HEAP_PAGE/etc. $5 is *everything* after # the hostname, as long as that everything is the empty string, # a slash, or something ending in $PROFILE_PAGE/$HEAP_PAGE/etc. # So "$7 || $5" is $PROFILE_PAGE/etc if there, or else it's "/" or "". - return ($2, $3, $6, $7 || $5); + return ($1 || "http", $2, $3, $6, $7 || $5); } return (); } # We fetch symbols from the first profile argument. sub SymbolPageURL { - my ($host, $port, $prefix, $path) = ParseProfileURL($main::pfile_args[0]); - return "http://$host:$port$prefix$SYMBOL_PAGE"; + my ($scheme, $host, $port, $prefix, $path) = ParseProfileURL($main::pfile_args[0]); + return "$scheme://$host:$port$prefix$SYMBOL_PAGE"; } sub FetchProgramName() { - my ($host, $port, $prefix, $path) = ParseProfileURL($main::pfile_args[0]); - my $url = "http://$host:$port$prefix$PROGRAM_NAME_PAGE"; + my ($scheme, $host, $port, $prefix, $path) = ParseProfileURL($main::pfile_args[0]); + my $url = "$scheme://$host:$port$prefix$PROGRAM_NAME_PAGE"; my $command_line = "$CURL -s '$url'"; open(CMDLINE, "$command_line |") or error($command_line); my $cmdline = <CMDLINE>; @@ -3128,7 +3137,7 @@ sub BaseName { sub MakeProfileBaseName { my ($binary_name, $profile_name) = @_; - my ($host, $port, $prefix, $path) = ParseProfileURL($profile_name); + my ($scheme, $host, $port, $prefix, $path) = ParseProfileURL($profile_name); my $binary_shortname = BaseName($binary_name); return sprintf("%s.%s.%s-port%s", $binary_shortname, $main::op_time, $host, $port); @@ -3143,7 +3152,7 @@ sub FetchDynamicProfile { if (!IsProfileURL($profile_name)) { return $profile_name; } else { - my ($host, $port, $prefix, $path) = ParseProfileURL($profile_name); + my ($scheme, $host, $port, $prefix, $path) = ParseProfileURL($profile_name); if ($path eq "" || $path eq "/") { # Missing type specifier defaults to cpu-profile $path = $PROFILE_PAGE; @@ -3155,7 +3164,7 @@ sub FetchDynamicProfile { my $curl_timeout; if (($path =~ m/$PROFILE_PAGE/) || ($path =~ m/$PMUPROFILE_PAGE/)) { if ($path =~ m/$PROFILE_PAGE/) { - $url = sprintf("http://$host:$port$prefix$path?seconds=%d", + $url = sprintf("$scheme://$host:$port$prefix$path?seconds=%d", $main::opt_seconds); } else { if ($profile_name =~ m/[?]/) { @@ -3163,7 +3172,7 @@ sub FetchDynamicProfile { } else { $profile_name .= "?" } - $url = sprintf("http://$profile_name" . "seconds=%d", + $url = sprintf("$scheme://$profile_name" . "seconds=%d", $main::opt_seconds); } $curl_timeout = sprintf("--max-time %d", @@ -3174,7 +3183,7 @@ sub FetchDynamicProfile { my $suffix = $path; $suffix =~ s,/,.,g; $profile_file .= "$suffix"; - $url = "http://$host:$port$prefix$path"; + $url = "$scheme://$host:$port$prefix$path"; $curl_timeout = ""; } @@ -3201,6 +3210,10 @@ sub FetchDynamicProfile { } (system($cmd) == 0) || error("Failed to get profile: $cmd: $!\n"); + open(TMPPROF, "$tmp_profile") || error("Cannot open $tmp_profile: $!\n"); + my $line = <TMPPROF>; + close(TMPPROF); + $line !~ /^Could not enable CPU profiling/ || error($line); (system("mv $tmp_profile $real_profile") == 0) || error("Unable to rename profile\n"); print STDERR "Wrote profile to $real_profile\n"; $main::collected_profile = $real_profile; @@ -3753,15 +3766,19 @@ sub ReadHeapProfile { } else { # Remote-heap version 1 my $ratio; - $ratio = (($s1*1.0)/$n1)/($sample_adjustment); - if ($ratio < 1) { - $n1 /= $ratio; - $s1 /= $ratio; + if ($n1 > 0) { + $ratio = (($s1*1.0)/$n1)/($sample_adjustment); + if ($ratio < 1) { + $n1 /= $ratio; + $s1 /= $ratio; + } } - $ratio = (($s2*1.0)/$n2)/($sample_adjustment); - if ($ratio < 1) { - $n2 /= $ratio; - $s2 /= $ratio; + if ($n2 > 0) { + $ratio = (($s2*1.0)/$n2)/($sample_adjustment); + if ($ratio < 1) { + $n2 /= $ratio; + $s2 /= $ratio; + } } } } @@ -4595,6 +4612,7 @@ sub ConfigureObjTools { # in the same directory as pprof. $obj_tool_map{"nm_pdb"} = "nm-pdb"; $obj_tool_map{"addr2line_pdb"} = "addr2line-pdb"; + $obj_tool_map{"is_windows"} = "true"; } if ($file_type =~ /Mach-O/) { @@ -4802,16 +4820,13 @@ sub GetProcedureBoundaries { " $image 2>/dev/null $cppfilt_flag", "$nm -D -n $flatten_flag $demangle_flag" . " $image 2>/dev/null $cppfilt_flag", - # 6nm is for Go binaries - "6nm $image 2>/dev/null | sort"); - - # If the executable is an MS Windows PDB-format executable, we'll - # have set up obj_tool_map("nm_pdb"). In this case, we actually - # want to use both unix nm and windows-specific nm_pdb, since - # PDB-format executables can apparently include dwarf .o files. - if (exists $obj_tool_map{"nm_pdb"}) { - my $nm_pdb = $obj_tool_map{"nm_pdb"}; - push(@nm_commands, "$nm_pdb --demangle $image 2>/dev/null"); + # go tool nm is for Go binaries + "go tool nm $image 2>/dev/null | sort"); + + # If the executable is an MS Windows Go executable, we'll + # have set up obj_tool_map("is_windows"). + if (exists $obj_tool_map{"is_windows"}) { + @nm_commands = ("go tool nm $image 2>/dev/null | sort"); } foreach my $nm_command (@nm_commands) { diff --git a/misc/swig/callback/Makefile b/misc/swig/callback/Makefile deleted file mode 100644 index 0ca33ef60..000000000 --- a/misc/swig/callback/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2011 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. - -include ../../../src/Make.inc - -TARG=swig/callback -SWIGFILES=\ - callback.swigcxx - -CLEANFILES+=run - -include ../../../src/Make.pkg - -%: install %.go - $(GC) $(GCFLAGS) $(GCIMPORTS) $*.go - $(LD) $(SWIG_RPATH) -o $@ $*.$O diff --git a/misc/swig/callback/callback.go b/misc/swig/callback/callback.go new file mode 100644 index 000000000..39c1719d2 --- /dev/null +++ b/misc/swig/callback/callback.go @@ -0,0 +1,11 @@ +// 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 callback + +type GoCallback struct{} + +func (p *GoCallback) Run() string { + return "GoCallback.Run" +} diff --git a/misc/swig/callback/callback_test.go b/misc/swig/callback/callback_test.go new file mode 100644 index 000000000..cf008fb54 --- /dev/null +++ b/misc/swig/callback/callback_test.go @@ -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 callback_test + +import ( + "../callback" + "testing" +) + +func TestCall(t *testing.T) { + c := callback.NewCaller() + cb := callback.NewCallback() + + c.SetCallback(cb) + s := c.Call() + if s != "Callback::run" { + t.Errorf("unexpected string from Call: %q", s) + } + c.DelCallback() +} + +func TestCallback(t *testing.T) { + c := callback.NewCaller() + cb := callback.NewDirectorCallback(&callback.GoCallback{}) + c.SetCallback(cb) + s := c.Call() + if s != "GoCallback.Run" { + t.Errorf("unexpected string from Call with callback: %q", s) + } + c.DelCallback() + callback.DeleteDirectorCallback(cb) +} diff --git a/misc/swig/callback/run.go b/misc/swig/callback/run.go deleted file mode 100644 index b3f13ad90..000000000 --- a/misc/swig/callback/run.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2011 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 main - -import ( - "fmt" - "swig/callback" -) - -type GoCallback struct{} - -func (p *GoCallback) Run() string { - return "GoCallback.Run" -} - -func main() { - c := callback.NewCaller() - cb := callback.NewCallback() - - c.SetCallback(cb) - s := c.Call() - fmt.Println(s) - if s != "Callback::run" { - panic(s) - } - c.DelCallback() - - cb = callback.NewDirectorCallback(&GoCallback{}) - c.SetCallback(cb) - s = c.Call() - fmt.Println(s) - if s != "GoCallback.Run" { - panic(s) - } - c.DelCallback() - callback.DeleteDirectorCallback(cb) -} diff --git a/misc/swig/stdio/file.swig b/misc/swig/stdio/file.swig index 57c623f8f..8ba341d08 100644 --- a/misc/swig/stdio/file.swig +++ b/misc/swig/stdio/file.swig @@ -6,6 +6,19 @@ %{ #include <stdio.h> +#include <stdlib.h> %} -int puts(const char *); +%typemap(gotype) const char * "string" +%typemap(in) const char * %{ + $1 = malloc($input.n + 1); + memcpy($1, $input.p, $input.n); + $1[$input.n] = '\0'; +%} +%typemap(freearg) const char * %{ + free($1); +%} + +FILE *fopen(const char *name, const char *mode); +int fclose(FILE *); +int fgetc(FILE *); diff --git a/misc/swig/stdio/file_test.go b/misc/swig/stdio/file_test.go new file mode 100644 index 000000000..6478a7cf3 --- /dev/null +++ b/misc/swig/stdio/file_test.go @@ -0,0 +1,22 @@ +// 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 file + +import "testing" + +// Open this file itself and verify that the first few characters are +// as expected. +func TestRead(t *testing.T) { + f := Fopen("file_test.go", "r") + if f == nil { + t.Fatal("fopen failed") + } + if Fgetc(f) != '/' || Fgetc(f) != '/' || Fgetc(f) != ' ' || Fgetc(f) != 'C' { + t.Error("read unexpected characters") + } + if Fclose(f) != 0 { + t.Error("fclose failed") + } +} diff --git a/misc/swig/stdio/hello.go b/misc/swig/stdio/hello.go deleted file mode 100644 index eec294278..000000000 --- a/misc/swig/stdio/hello.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2011 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 main - -import "swig/file" - -func main() { - file.Puts("Hello, world") -} diff --git a/misc/vim/autoload/go/complete.vim b/misc/vim/autoload/go/complete.vim index d4ae3b97f..cc1013b7d 100644 --- a/misc/vim/autoload/go/complete.vim +++ b/misc/vim/autoload/go/complete.vim @@ -29,21 +29,43 @@ if len(s:goarch) == 0 endif function! go#complete#Package(ArgLead, CmdLine, CursorPos) - let goroot = $GOROOT - if len(goroot) == 0 - " should not occur. - return [] + let dirs = [] + + if executable('go') + let goroot = substitute(system('go env GOROOT'), '\n', '', 'g') + if v:shell_error + echo '\'go env GOROOT\' failed' + endif + else + let goroot = $GOROOT endif + + if len(goroot) != 0 && isdirectory(goroot) + let dirs += [ goroot ] + endif + + let workspaces = split($GOPATH, ':') + if workspaces != [] + let dirs += workspaces + endif + + if len(dirs) == 0 + " should not happen + return [] + endif + let ret = {} - let root = expand(goroot.'/pkg/'.s:goos.'_'.s:goarch) - for i in split(globpath(root, a:ArgLead.'*'), "\n") - if isdirectory(i) - let i .= '/' - elseif i !~ '\.a$' - continue - endif - let i = substitute(substitute(i[len(root)+1:], '[\\]', '/', 'g'), '\.a$', '', 'g') - let ret[i] = i + for dir in dirs + let root = expand(dir . '/pkg/' . s:goos . '_' . s:goarch) + for i in split(globpath(root, a:ArgLead.'*'), "\n") + if isdirectory(i) + let i .= '/' + elseif i !~ '\.a$' + continue + endif + let i = substitute(substitute(i[len(root)+1:], '[\\]', '/', 'g'), '\.a$', '', 'g') + let ret[i] = i + endfor endfor return sort(keys(ret)) endfunction diff --git a/misc/vim/ftplugin/go/fmt.vim b/misc/vim/ftplugin/go/fmt.vim index 0ee44cd59..30814fdfd 100644 --- a/misc/vim/ftplugin/go/fmt.vim +++ b/misc/vim/ftplugin/go/fmt.vim @@ -12,6 +12,9 @@ " It tries to preserve cursor position and avoids " replacing the buffer with stderr output. " +if exists("b:did_ftplugin_go_fmt") + finish +endif command! -buffer Fmt call s:GoFormat() @@ -41,4 +44,6 @@ function! s:GoFormat() call winrestview(view) endfunction +let b:did_ftplugin_go_fmt = 1 + " vim:ts=4:sw=4:et diff --git a/misc/vim/ftplugin/go/godoc.vim b/misc/vim/ftplugin/go/godoc.vim deleted file mode 100644 index 55195a674..000000000 --- a/misc/vim/ftplugin/go/godoc.vim +++ /dev/null @@ -1,13 +0,0 @@ -" Copyright 2011 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. -" -" godoc.vim: Vim command to see godoc. - -if exists("b:did_ftplugin") - finish -endif - -silent! nmap <buffer> <silent> K <Plug>(godoc-keyword) - -" vim:ts=4:sw=4:et diff --git a/misc/vim/ftplugin/go/import.vim b/misc/vim/ftplugin/go/import.vim index 6705a476b..1d969e460 100644 --- a/misc/vim/ftplugin/go/import.vim +++ b/misc/vim/ftplugin/go/import.vim @@ -12,7 +12,7 @@ " in the current Go buffer, using proper style and ordering. " If {path} is already being imported, an error will be " displayed and the buffer will be untouched. -" +" " :ImportAs {localname} {path} " " Same as Import, but uses a custom local name for the package. @@ -32,7 +32,7 @@ " The backslash is the default maplocalleader, so it is possible that " your vim is set to use a different character (:help maplocalleader). " -if exists("b:did_ftplugin") +if exists("b:did_ftplugin_go_import") finish endif @@ -58,6 +58,12 @@ function! s:SwitchImport(enabled, localname, path) return endif + " Extract any site prefix (e.g. github.com/). + " If other imports with the same prefix are grouped separately, + " we will add this new import with them. + " Only up to and including the first slash is used. + let siteprefix = matchstr(path, "^[^/]*/") + let qpath = '"' . path . '"' if a:localname != '' let qlocalpath = a:localname . ' ' . qpath @@ -83,16 +89,31 @@ function! s:SwitchImport(enabled, localname, path) let appendstr = qlocalpath let indentstr = 1 let appendline = line + let firstblank = -1 + let lastprefix = "" while line <= line("$") let line = line + 1 let linestr = getline(line) let m = matchlist(getline(line), '^\()\|\(\s\+\)\(\S*\s*\)"\(.\+\)"\)') if empty(m) + if siteprefix == "" && a:enabled + " must be in the first group + break + endif + " record this position, but keep looking + if firstblank < 0 + let firstblank = line + endif continue endif if m[1] == ')' + " if there's no match, add it to the first group + if appendline < 0 && firstblank >= 0 + let appendline = firstblank + endif break endif + let lastprefix = matchstr(m[4], "^[^/]*/") if a:localname != '' && m[3] != '' let qlocalpath = printf('%-' . (len(m[3])-1) . 's %s', a:localname, qpath) endif @@ -103,7 +124,16 @@ function! s:SwitchImport(enabled, localname, path) let deleteline = line break elseif m[4] < path - let appendline = line + " don't set candidate position if we have a site prefix, + " we've passed a blank line, and this doesn't share the same + " site prefix. + if siteprefix == "" || firstblank < 0 || match(m[4], "^" . siteprefix) >= 0 + let appendline = line + endif + elseif siteprefix != "" && match(m[4], "^" . siteprefix) >= 0 + " first entry of site group + let appendline = line - 1 + break endif endwhile break @@ -198,4 +228,6 @@ function! s:Error(s) echohl Error | echo a:s | echohl None endfunction +let b:did_ftplugin_go_import = 1 + " vim:ts=4:sw=4:et diff --git a/misc/vim/ftplugin/go/test.sh b/misc/vim/ftplugin/go/test.sh new file mode 100755 index 000000000..a6e31d8a3 --- /dev/null +++ b/misc/vim/ftplugin/go/test.sh @@ -0,0 +1,78 @@ +#!/bin/bash -e +# +# 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. +# +# Tests for import.vim. + +cd $(dirname $0) + +cat > base.go <<EOF +package test + +import ( + "bytes" + "io" + "net" + + "mycorp/foo" +) +EOF + +fail=0 + +# usage: test_one command pattern +# Pattern is a PCRE expression that will match across lines. +test_one() { + echo 2>&1 -n "$1: " + vim -e -s -u /dev/null -U /dev/null --noplugin -c "source import.vim" \ + -c "$1" -c 'wq! test.go' base.go + # ensure blank lines are treated correctly + if ! gofmt test.go | cmp test.go; then + echo 2>&1 "gofmt conflict" + gofmt test.go | diff -u test.go - | sed "s/^/ /" 2>&1 + fail=1 + return + fi + if ! grep -P -q "(?s)$2" test.go; then + echo 2>&1 "$2 did not match" + cat test.go | sed "s/^/ /" 2>&1 + fail=1 + return + fi + echo 2>&1 "ok" +} + +# Tests for Import + +test_one "Import baz" '"baz".*"bytes"' +test_one "Import io/ioutil" '"io".*"io/ioutil".*"net"' +test_one "Import myc" '"io".*"myc".*"net"' # prefix of a site prefix +test_one "Import nat" '"io".*"nat".*"net"' +test_one "Import net/http" '"net".*"net/http".*"mycorp/foo"' +test_one "Import zoo" '"net".*"zoo".*"mycorp/foo"' +test_one "Import mycorp/bar" '"net".*"mycorp/bar".*"mycorp/foo"' +test_one "Import mycorp/goo" '"net".*"mycorp/foo".*"mycorp/goo"' + +# Tests for Drop + +cat > base.go <<EOF +package test + +import ( + "foo" + + "something" + "zoo" +) +EOF + +test_one "Drop something" '\([^"]*"foo"[^"]*"zoo"[^"]*\)' + +rm -f base.go test.go +if [ $fail -gt 0 ]; then + echo 2>&1 "FAIL" + exit 1 +fi +echo 2>&1 "PASS" diff --git a/misc/vim/plugin/godoc.vim b/misc/vim/plugin/godoc.vim index fdb496631..a9abb7ae6 100644 --- a/misc/vim/plugin/godoc.vim +++ b/misc/vim/plugin/godoc.vim @@ -72,7 +72,7 @@ function! s:Godoc(...) if !len(word) let word = expand('<cword>') endif - let word = substitute(word, '[^a-zA-Z0-9\/]', '', 'g') + let word = substitute(word, '[^a-zA-Z0-9\\/._~-]', '', 'g') if !len(word) return endif diff --git a/misc/vim/readme.txt b/misc/vim/readme.txt index fe15da993..cb3a52073 100644 --- a/misc/vim/readme.txt +++ b/misc/vim/readme.txt @@ -1,9 +1,13 @@ Vim plugins for Go (http://golang.org) ====================================== -To use all the Vim plugins, add these lines to your vimrc. +To use all the Vim plugins, add these lines to your $HOME/.vimrc. - set rtp+=$GOROOT/misc/vim + " Some Linux distributions set filetype in /etc/vimrc. + " Clear filetype flags before changing runtimepath to force Vim to reload them. + filetype off + filetype plugin indent off + set runtimepath+=$GOROOT/misc/vim filetype plugin indent on syntax on @@ -72,5 +76,4 @@ To install godoc plugin: 1. Same as 1 above. 2. Copy or link plugin/godoc.vim to $HOME/.vim/plugin/godoc, syntax/godoc.vim to $HOME/.vim/syntax/godoc.vim, - ftplugin/go/godoc.vim to $HOME/.vim/ftplugin/go/godoc.vim. and autoload/go/complete.vim to $HOME/.vim/autoload/go/complete.vim. diff --git a/misc/zsh/go b/misc/zsh/go index 23afa9656..dce25547d 100644 --- a/misc/zsh/go +++ b/misc/zsh/go @@ -88,7 +88,7 @@ __go_tool_complete() { "-cpu[values of GOMAXPROCS to use]:number list" \ "-run[run tests and examples matching regexp]:regexp" \ "-bench[run benchmarks matching regexp]:regexp" \ - "-benchtime[run each benchmark during n seconds]:duration" \ + "-benchtime[run each benchmark until taking this long]:duration" \ "-timeout[kill test after that duration]:duration" \ "-cpuprofile[write CPU profile to file]:file:_files" \ "-memprofile[write heap profile to file]:file:_files" \ |