##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r644:62834191 merge stable
parent child Browse files
Show More
@@ -1,6 +1,6 b''
1 [bumpversion]
1 [bumpversion]
2 current_version = 4.15.2
2 current_version = 4.16.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:vcsserver/VERSION]
5 [bumpversion:file:vcsserver/VERSION]
6
6
@@ -1,16 +1,14 b''
1 [DEFAULT]
1 [DEFAULT]
2 done = false
2 done = false
3
3
4 [task:bump_version]
4 [task:bump_version]
5 done = true
5 done = true
6
6
7 [task:fixes_on_stable]
7 [task:fixes_on_stable]
8 done = true
9
8
10 [task:pip2nix_generated]
9 [task:pip2nix_generated]
11 done = true
12
10
13 [release]
11 [release]
14 state = prepared
12 state = in_progress
15 version = 4.15.2
13 version = 4.16.0
16
14
@@ -1,18 +1,23 b''
1
1
2 .PHONY: clean test test-clean test-only
2 .PHONY: clean test test-clean test-only generate-pkgs
3
3
4
4
5 clean:
5 clean:
6 make test-clean
6 make test-clean
7 find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' \) -exec rm '{}' ';'
7 find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' -o -iname '*.orig' \) -exec rm '{}' ';'
8
8
9 test:
9 test:
10 make test-clean
10 make test-clean
11 make test-only
11 make test-only
12
12
13 test-clean:
13 test-clean:
14 rm -rf coverage.xml htmlcov junit.xml pylint.log result
14 rm -rf coverage.xml htmlcov junit.xml pylint.log result
15 find . -type d -name "__pycache__" -prune -exec rm -rf '{}' ';'
15 find . -type d -name "__pycache__" -prune -exec rm -rf '{}' ';'
16
16
17 test-only:
17 test-only:
18 PYTHONHASHSEED=random py.test -vv -r xw -p no:sugar --cov=vcsserver --cov-report=term-missing --cov-report=html vcsserver
18 PYTHONHASHSEED=random \
19 py.test -x -vv -r xw -p no:sugar \
20 --cov=vcsserver --cov-report=term-missing --cov-report=html vcsserver
21
22 generate-pkgs:
23 nix-shell pkgs/shell-generate.nix --command "pip2nix generate --licenses"
@@ -1,196 +1,197 b''
1 # Nix environment for the community edition
1 # Nix environment for the community edition
2 #
2 #
3 # This shall be as lean as possible, just producing the rhodecode-vcsserver
3 # This shall be as lean as possible, just producing the rhodecode-vcsserver
4 # derivation. For advanced tweaks to pimp up the development environment we use
4 # derivation. For advanced tweaks to pimp up the development environment we use
5 # "shell.nix" so that it does not have to clutter this file.
5 # "shell.nix" so that it does not have to clutter this file.
6
6
7 args@
7 args@
8 { pythonPackages ? "python27Packages"
8 { system ? builtins.currentSystem
9 , pythonPackages ? "python27Packages"
9 , pythonExternalOverrides ? self: super: {}
10 , pythonExternalOverrides ? self: super: {}
10 , doCheck ? false
11 , doCheck ? false
11 , ...
12 , ...
12 }:
13 }:
13
14
14 let
15 let
15 pkgs_ = (import <nixpkgs> {});
16 pkgs_ = args.pkgs or (import <nixpkgs> { inherit system; });
16 in
17 in
17
18
18 let
19 let
19 pkgs = import <nixpkgs> {
20 pkgs = import <nixpkgs> {
20 overlays = [
21 overlays = [
21 (import ./pkgs/overlays.nix)
22 (import ./pkgs/overlays.nix)
22 ];
23 ];
23 inherit
24 inherit
24 (pkgs_)
25 (pkgs_)
25 system;
26 system;
26 };
27 };
27
28
28 # Works with the new python-packages, still can fallback to the old
29 # Works with the new python-packages, still can fallback to the old
29 # variant.
30 # variant.
30 basePythonPackagesUnfix = basePythonPackages.__unfix__ or (
31 basePythonPackagesUnfix = basePythonPackages.__unfix__ or (
31 self: basePythonPackages.override (a: { inherit self; }));
32 self: basePythonPackages.override (a: { inherit self; }));
32
33
33 # Evaluates to the last segment of a file system path.
34 # Evaluates to the last segment of a file system path.
34 basename = path: with pkgs.lib; last (splitString "/" path);
35 basename = path: with pkgs.lib; last (splitString "/" path);
35
36
36 # source code filter used as arugment to builtins.filterSource.
37 # source code filter used as arugment to builtins.filterSource.
37 src-filter = path: type: with pkgs.lib;
38 src-filter = path: type: with pkgs.lib;
38 let
39 let
39 ext = last (splitString "." path);
40 ext = last (splitString "." path);
40 in
41 in
41 !builtins.elem (basename path) [
42 !builtins.elem (basename path) [
42 ".git" ".hg" "__pycache__" ".eggs" ".idea" ".dev"
43 ".git" ".hg" "__pycache__" ".eggs" ".idea" ".dev"
43 "node_modules" "node_binaries"
44 "node_modules" "node_binaries"
44 "build" "data" "result" "tmp"] &&
45 "build" "data" "result" "tmp"] &&
45 !builtins.elem ext ["egg-info" "pyc"] &&
46 !builtins.elem ext ["egg-info" "pyc"] &&
46 # TODO: johbo: This check is wrong, since "path" contains an absolute path,
47 # TODO: johbo: This check is wrong, since "path" contains an absolute path,
47 # it would still be good to restore it since we want to ignore "result-*".
48 # it would still be good to restore it since we want to ignore "result-*".
48 !hasPrefix "result" path;
49 !hasPrefix "result" path;
49
50
50 sources =
51 sources =
51 let
52 let
52 inherit
53 inherit
53 (pkgs.lib)
54 (pkgs.lib)
54 all
55 all
55 isString
56 isString
56 attrValues;
57 attrValues;
57 sourcesConfig = pkgs.config.rc.sources or {};
58 sourcesConfig = pkgs.config.rc.sources or {};
58 in
59 in
59 # Ensure that sources are configured as strings. Using a path
60 # Ensure that sources are configured as strings. Using a path
60 # would result in a copy into the nix store.
61 # would result in a copy into the nix store.
61 assert all isString (attrValues sourcesConfig);
62 assert all isString (attrValues sourcesConfig);
62 sourcesConfig;
63 sourcesConfig;
63
64
64 version = builtins.readFile "${rhodecode-vcsserver-src}/vcsserver/VERSION";
65 version = builtins.readFile "${rhodecode-vcsserver-src}/vcsserver/VERSION";
65 rhodecode-vcsserver-src = builtins.filterSource src-filter ./.;
66 rhodecode-vcsserver-src = builtins.filterSource src-filter ./.;
66
67
67 pythonLocalOverrides = self: super: {
68 pythonLocalOverrides = self: super: {
68 rhodecode-vcsserver =
69 rhodecode-vcsserver =
69 let
70 let
70 releaseName = "RhodeCodeVCSServer-${version}";
71 releaseName = "RhodeCodeVCSServer-${version}";
71 in super.rhodecode-vcsserver.override (attrs: {
72 in super.rhodecode-vcsserver.override (attrs: {
72 inherit
73 inherit
73 doCheck
74 doCheck
74 version;
75 version;
75
76
76 name = "rhodecode-vcsserver-${version}";
77 name = "rhodecode-vcsserver-${version}";
77 releaseName = releaseName;
78 releaseName = releaseName;
78 src = rhodecode-vcsserver-src;
79 src = rhodecode-vcsserver-src;
79 dontStrip = true; # prevent strip, we don't need it.
80 dontStrip = true; # prevent strip, we don't need it.
80
81
81 # expose following attributed outside
82 # expose following attributed outside
82 passthru = {
83 passthru = {
83 pythonPackages = self;
84 pythonPackages = self;
84 };
85 };
85
86
86 propagatedBuildInputs =
87 propagatedBuildInputs =
87 attrs.propagatedBuildInputs or [] ++ [
88 attrs.propagatedBuildInputs or [] ++ [
88 pkgs.git
89 pkgs.git
89 pkgs.subversion
90 pkgs.subversion
90 ];
91 ];
91
92
92 # set some default locale env variables
93 # set some default locale env variables
93 LC_ALL = "en_US.UTF-8";
94 LC_ALL = "en_US.UTF-8";
94 LOCALE_ARCHIVE =
95 LOCALE_ARCHIVE =
95 if pkgs.stdenv.isLinux
96 if pkgs.stdenv.isLinux
96 then "${pkgs.glibcLocales}/lib/locale/locale-archive"
97 then "${pkgs.glibcLocales}/lib/locale/locale-archive"
97 else "";
98 else "";
98
99
99 # Add bin directory to path so that tests can find 'vcsserver'.
100 # Add bin directory to path so that tests can find 'vcsserver'.
100 preCheck = ''
101 preCheck = ''
101 export PATH="$out/bin:$PATH"
102 export PATH="$out/bin:$PATH"
102 '';
103 '';
103
104
104 # custom check phase for testing
105 # custom check phase for testing
105 checkPhase = ''
106 checkPhase = ''
106 runHook preCheck
107 runHook preCheck
107 PYTHONHASHSEED=random py.test -vv -p no:sugar -r xw --cov-config=.coveragerc --cov=vcsserver --cov-report=term-missing vcsserver
108 PYTHONHASHSEED=random py.test -vv -p no:sugar -r xw --cov-config=.coveragerc --cov=vcsserver --cov-report=term-missing vcsserver
108 runHook postCheck
109 runHook postCheck
109 '';
110 '';
110
111
111 postCheck = ''
112 postCheck = ''
112 echo "Cleanup of vcsserver/tests"
113 echo "Cleanup of vcsserver/tests"
113 rm -rf $out/lib/${self.python.libPrefix}/site-packages/vcsserver/tests
114 rm -rf $out/lib/${self.python.libPrefix}/site-packages/vcsserver/tests
114 '';
115 '';
115
116
116 postInstall = ''
117 postInstall = ''
117 echo "Writing vcsserver meta information for rccontrol to nix-support/rccontrol"
118 echo "Writing vcsserver meta information for rccontrol to nix-support/rccontrol"
118 mkdir -p $out/nix-support/rccontrol
119 mkdir -p $out/nix-support/rccontrol
119 cp -v vcsserver/VERSION $out/nix-support/rccontrol/version
120 cp -v vcsserver/VERSION $out/nix-support/rccontrol/version
120 echo "DONE: vcsserver meta information for rccontrol written"
121 echo "DONE: vcsserver meta information for rccontrol written"
121
122
122 mkdir -p $out/etc
123 mkdir -p $out/etc
123 cp configs/production.ini $out/etc
124 cp configs/production.ini $out/etc
124 echo "DONE: saved vcsserver production.ini into $out/etc"
125 echo "DONE: saved vcsserver production.ini into $out/etc"
125
126
126 # python based programs need to be wrapped
127 # python based programs need to be wrapped
127 mkdir -p $out/bin
128 mkdir -p $out/bin
128 ln -s ${self.python}/bin/python $out/bin/
129 ln -s ${self.python}/bin/python $out/bin/
129 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
130 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
130 ln -s ${self.pyramid}/bin/prequest $out/bin/
131 ln -s ${self.pyramid}/bin/prequest $out/bin/
131 ln -s ${self.pyramid}/bin/pserve $out/bin/
132 ln -s ${self.pyramid}/bin/pserve $out/bin/
132
133
133 # Symlink version control utilities
134 # Symlink version control utilities
134 # We ensure that always the correct version is available as a symlink.
135 # We ensure that always the correct version is available as a symlink.
135 # So that users calling them via the profile path will always use the
136 # So that users calling them via the profile path will always use the
136 # correct version. Wrapping is required so those can "import"
137 # correct version. Wrapping is required so those can "import"
137 # vcsserver python hooks.
138 # vcsserver python hooks.
138
139
139 ln -s ${pkgs.git}/bin/git $out/bin
140 ln -s ${pkgs.git}/bin/git $out/bin
140 ln -s ${self.mercurial}/bin/hg $out/bin
141 ln -s ${self.mercurial}/bin/hg $out/bin
141 ln -s ${pkgs.subversion}/bin/svn* $out/bin
142 ln -s ${pkgs.subversion}/bin/svn* $out/bin
142
143
143 echo "DONE: created symlinks into $out/bin"
144 echo "DONE: created symlinks into $out/bin"
144 DEPS="$out/bin/*"
145 DEPS="$out/bin/*"
145
146
146 # wrap only dependency scripts, they require to have full PYTHONPATH set
147 # wrap only dependency scripts, they require to have full PYTHONPATH set
147 # to be able to import all packages
148 # to be able to import all packages
148 for file in $DEPS;
149 for file in $DEPS;
149 do
150 do
150 wrapProgram $file \
151 wrapProgram $file \
151 --prefix PATH : $PATH \
152 --prefix PATH : $PATH \
152 --prefix PYTHONPATH : $PYTHONPATH \
153 --prefix PYTHONPATH : $PYTHONPATH \
153 --set PYTHONHASHSEED random
154 --set PYTHONHASHSEED random
154 done
155 done
155
156
156 echo "DONE: vcsserver binary wrapping"
157 echo "DONE: vcsserver binary wrapping"
157
158
158 '';
159 '';
159
160
160 });
161 });
161 };
162 };
162
163
163 basePythonPackages = with builtins;
164 basePythonPackages = with builtins;
164 if isAttrs pythonPackages then
165 if isAttrs pythonPackages then
165 pythonPackages
166 pythonPackages
166 else
167 else
167 getAttr pythonPackages pkgs;
168 getAttr pythonPackages pkgs;
168
169
169 pythonGeneratedPackages = import ./pkgs/python-packages.nix {
170 pythonGeneratedPackages = import ./pkgs/python-packages.nix {
170 inherit
171 inherit
171 pkgs;
172 pkgs;
172 inherit
173 inherit
173 (pkgs)
174 (pkgs)
174 fetchurl
175 fetchurl
175 fetchgit
176 fetchgit
176 fetchhg;
177 fetchhg;
177 };
178 };
178
179
179 pythonVCSServerOverrides = import ./pkgs/python-packages-overrides.nix {
180 pythonVCSServerOverrides = import ./pkgs/python-packages-overrides.nix {
180 inherit
181 inherit
181 pkgs
182 pkgs
182 basePythonPackages;
183 basePythonPackages;
183 };
184 };
184
185
185
186
186 # Apply all overrides and fix the final package set
187 # Apply all overrides and fix the final package set
187 myPythonPackagesUnfix = with pkgs.lib;
188 myPythonPackagesUnfix = with pkgs.lib;
188 (extends pythonExternalOverrides
189 (extends pythonExternalOverrides
189 (extends pythonLocalOverrides
190 (extends pythonLocalOverrides
190 (extends pythonVCSServerOverrides
191 (extends pythonVCSServerOverrides
191 (extends pythonGeneratedPackages
192 (extends pythonGeneratedPackages
192 basePythonPackagesUnfix))));
193 basePythonPackagesUnfix))));
193
194
194 myPythonPackages = (pkgs.lib.fix myPythonPackagesUnfix);
195 myPythonPackages = (pkgs.lib.fix myPythonPackagesUnfix);
195
196
196 in myPythonPackages.rhodecode-vcsserver
197 in myPythonPackages.rhodecode-vcsserver
@@ -1,47 +1,47 b''
1 self: super: {
1 self: super: {
2 # bump GIT version
2 # bump GIT version
3 git = super.lib.overrideDerivation super.git (oldAttrs: {
3 git = super.lib.overrideDerivation super.git (oldAttrs: {
4 name = "git-2.19.1";
4 name = "git-2.19.2";
5 src = self.fetchurl {
5 src = self.fetchurl {
6 url = "https://www.kernel.org/pub/software/scm/git/git-2.19.1.tar.xz";
6 url = "https://www.kernel.org/pub/software/scm/git/git-2.19.2.tar.xz";
7 sha256 = "1dfv43lmdnxz42504jc89sihbv1d4d6kgqcz3c5ji140kfm5cl1l";
7 sha256 = "1scbggzghkzzfqg4ky3qh7h9w87c3zya4ls5disz7dbx56is7sgw";
8 };
8 };
9
9
10 # patches come from: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/version-management/git-and-tools/git
10 # patches come from: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/version-management/git-and-tools/git
11 patches = [
11 patches = [
12 ./patches/git/docbook2texi.patch
12 ./patches/git/docbook2texi.patch
13 ./patches/git/git-sh-i18n.patch
13 ./patches/git/git-sh-i18n.patch
14 ./patches/git/ssh-path.patch
14 ./patches/git/ssh-path.patch
15 ./patches/git/git-send-email-honor-PATH.patch
15 ./patches/git/git-send-email-honor-PATH.patch
16 ./patches/git/installCheck-path.patch
16 ./patches/git/installCheck-path.patch
17 ];
17 ];
18
18
19 });
19 });
20
20
21 # Override subversion derivation to
21 # Override subversion derivation to
22 # - activate python bindings
22 # - activate python bindings
23 subversion =
23 subversion =
24 let
24 let
25 subversionWithPython = super.subversion.override {
25 subversionWithPython = super.subversion.override {
26 httpSupport = true;
26 httpSupport = true;
27 pythonBindings = true;
27 pythonBindings = true;
28 python = self.python27Packages.python;
28 python = self.python27Packages.python;
29 };
29 };
30 in
30 in
31 super.lib.overrideDerivation subversionWithPython (oldAttrs: {
31 super.lib.overrideDerivation subversionWithPython (oldAttrs: {
32 name = "subversion-1.10.2";
32 name = "subversion-1.10.2";
33 src = self.fetchurl {
33 src = self.fetchurl {
34 url = "https://archive.apache.org/dist/subversion/subversion-1.10.2.tar.gz";
34 url = "https://archive.apache.org/dist/subversion/subversion-1.10.2.tar.gz";
35 sha256 = "0xv5z2bg0lw7057g913yc13f60nfj257wvmsq22pr33m4syf26sg";
35 sha256 = "0xv5z2bg0lw7057g913yc13f60nfj257wvmsq22pr33m4syf26sg";
36 };
36 };
37
37
38 ## use internal lz4/utf8proc because it is stable and shipped with SVN
38 ## use internal lz4/utf8proc because it is stable and shipped with SVN
39 configureFlags = oldAttrs.configureFlags ++ [
39 configureFlags = oldAttrs.configureFlags ++ [
40 " --with-lz4=internal"
40 " --with-lz4=internal"
41 " --with-utf8proc=internal"
41 " --with-utf8proc=internal"
42 ];
42 ];
43
43
44
44
45 });
45 });
46
46
47 }
47 }
@@ -1,950 +1,955 b''
1 # Generated by pip2nix 0.8.0.dev1
1 # Generated by pip2nix 0.8.0.dev1
2 # See https://github.com/johbo/pip2nix
2 # See https://github.com/johbo/pip2nix
3
3
4 { pkgs, fetchurl, fetchgit, fetchhg }:
4 { pkgs, fetchurl, fetchgit, fetchhg }:
5
5
6 self: super: {
6 self: super: {
7 "atomicwrites" = super.buildPythonPackage {
7 "atomicwrites" = super.buildPythonPackage {
8 name = "atomicwrites-1.2.1";
8 name = "atomicwrites-1.2.1";
9 doCheck = false;
9 doCheck = false;
10 src = fetchurl {
10 src = fetchurl {
11 url = "https://files.pythonhosted.org/packages/ac/ed/a311712ef6b4355035489f665e63e1a73f9eb371929e3c98e5efd451069e/atomicwrites-1.2.1.tar.gz";
11 url = "https://files.pythonhosted.org/packages/ac/ed/a311712ef6b4355035489f665e63e1a73f9eb371929e3c98e5efd451069e/atomicwrites-1.2.1.tar.gz";
12 sha256 = "1vmkbw9j0qammwxbxycrs39gvdg4lc2d4lk98kwf8ag2manyi6pc";
12 sha256 = "1vmkbw9j0qammwxbxycrs39gvdg4lc2d4lk98kwf8ag2manyi6pc";
13 };
13 };
14 meta = {
14 meta = {
15 license = [ pkgs.lib.licenses.mit ];
15 license = [ pkgs.lib.licenses.mit ];
16 };
16 };
17 };
17 };
18 "attrs" = super.buildPythonPackage {
18 "attrs" = super.buildPythonPackage {
19 name = "attrs-18.2.0";
19 name = "attrs-18.2.0";
20 doCheck = false;
20 doCheck = false;
21 src = fetchurl {
21 src = fetchurl {
22 url = "https://files.pythonhosted.org/packages/0f/9e/26b1d194aab960063b266170e53c39f73ea0d0d3f5ce23313e0ec8ee9bdf/attrs-18.2.0.tar.gz";
22 url = "https://files.pythonhosted.org/packages/0f/9e/26b1d194aab960063b266170e53c39f73ea0d0d3f5ce23313e0ec8ee9bdf/attrs-18.2.0.tar.gz";
23 sha256 = "0s9ydh058wmmf5v391pym877x4ahxg45dw6a0w4c7s5wgpigdjqh";
23 sha256 = "0s9ydh058wmmf5v391pym877x4ahxg45dw6a0w4c7s5wgpigdjqh";
24 };
24 };
25 meta = {
25 meta = {
26 license = [ pkgs.lib.licenses.mit ];
26 license = [ pkgs.lib.licenses.mit ];
27 };
27 };
28 };
28 };
29 "backports.shutil-get-terminal-size" = super.buildPythonPackage {
29 "backports.shutil-get-terminal-size" = super.buildPythonPackage {
30 name = "backports.shutil-get-terminal-size-1.0.0";
30 name = "backports.shutil-get-terminal-size-1.0.0";
31 doCheck = false;
31 doCheck = false;
32 src = fetchurl {
32 src = fetchurl {
33 url = "https://files.pythonhosted.org/packages/ec/9c/368086faa9c016efce5da3e0e13ba392c9db79e3ab740b763fe28620b18b/backports.shutil_get_terminal_size-1.0.0.tar.gz";
33 url = "https://files.pythonhosted.org/packages/ec/9c/368086faa9c016efce5da3e0e13ba392c9db79e3ab740b763fe28620b18b/backports.shutil_get_terminal_size-1.0.0.tar.gz";
34 sha256 = "107cmn7g3jnbkp826zlj8rrj19fam301qvaqf0f3905f5217lgki";
34 sha256 = "107cmn7g3jnbkp826zlj8rrj19fam301qvaqf0f3905f5217lgki";
35 };
35 };
36 meta = {
36 meta = {
37 license = [ pkgs.lib.licenses.mit ];
37 license = [ pkgs.lib.licenses.mit ];
38 };
38 };
39 };
39 };
40 "beautifulsoup4" = super.buildPythonPackage {
40 "beautifulsoup4" = super.buildPythonPackage {
41 name = "beautifulsoup4-4.6.3";
41 name = "beautifulsoup4-4.6.3";
42 doCheck = false;
42 doCheck = false;
43 src = fetchurl {
43 src = fetchurl {
44 url = "https://files.pythonhosted.org/packages/88/df/86bffad6309f74f3ff85ea69344a078fc30003270c8df6894fca7a3c72ff/beautifulsoup4-4.6.3.tar.gz";
44 url = "https://files.pythonhosted.org/packages/88/df/86bffad6309f74f3ff85ea69344a078fc30003270c8df6894fca7a3c72ff/beautifulsoup4-4.6.3.tar.gz";
45 sha256 = "041dhalzjciw6qyzzq7a2k4h1yvyk76xigp35hv5ibnn448ydy4h";
45 sha256 = "041dhalzjciw6qyzzq7a2k4h1yvyk76xigp35hv5ibnn448ydy4h";
46 };
46 };
47 meta = {
47 meta = {
48 license = [ pkgs.lib.licenses.mit ];
48 license = [ pkgs.lib.licenses.mit ];
49 };
49 };
50 };
50 };
51 "configobj" = super.buildPythonPackage {
51 "configobj" = super.buildPythonPackage {
52 name = "configobj-5.0.6";
52 name = "configobj-5.0.6";
53 doCheck = false;
53 doCheck = false;
54 propagatedBuildInputs = [
54 propagatedBuildInputs = [
55 self."six"
55 self."six"
56 ];
56 ];
57 src = fetchurl {
57 src = fetchurl {
58 url = "https://code.rhodecode.com/upstream/configobj/archive/a11ff0a0bd4fbda9e3a91267e720f88329efb4a6.tar.gz?md5=9916c524ea11a6c418217af6b28d4b3c";
58 url = "https://code.rhodecode.com/upstream/configobj/archive/a11ff0a0bd4fbda9e3a91267e720f88329efb4a6.tar.gz?md5=9916c524ea11a6c418217af6b28d4b3c";
59 sha256 = "1hhcxirwvg58grlfr177b3awhbq8hlx1l3lh69ifl1ki7lfd1s1x";
59 sha256 = "1hhcxirwvg58grlfr177b3awhbq8hlx1l3lh69ifl1ki7lfd1s1x";
60 };
60 };
61 meta = {
61 meta = {
62 license = [ pkgs.lib.licenses.bsdOriginal ];
62 license = [ pkgs.lib.licenses.bsdOriginal ];
63 };
63 };
64 };
64 };
65 "cov-core" = super.buildPythonPackage {
65 "cov-core" = super.buildPythonPackage {
66 name = "cov-core-1.15.0";
66 name = "cov-core-1.15.0";
67 doCheck = false;
67 doCheck = false;
68 propagatedBuildInputs = [
68 propagatedBuildInputs = [
69 self."coverage"
69 self."coverage"
70 ];
70 ];
71 src = fetchurl {
71 src = fetchurl {
72 url = "https://files.pythonhosted.org/packages/4b/87/13e75a47b4ba1be06f29f6d807ca99638bedc6b57fa491cd3de891ca2923/cov-core-1.15.0.tar.gz";
72 url = "https://files.pythonhosted.org/packages/4b/87/13e75a47b4ba1be06f29f6d807ca99638bedc6b57fa491cd3de891ca2923/cov-core-1.15.0.tar.gz";
73 sha256 = "0k3np9ymh06yv1ib96sb6wfsxjkqhmik8qfsn119vnhga9ywc52a";
73 sha256 = "0k3np9ymh06yv1ib96sb6wfsxjkqhmik8qfsn119vnhga9ywc52a";
74 };
74 };
75 meta = {
75 meta = {
76 license = [ pkgs.lib.licenses.mit ];
76 license = [ pkgs.lib.licenses.mit ];
77 };
77 };
78 };
78 };
79 "coverage" = super.buildPythonPackage {
79 "coverage" = super.buildPythonPackage {
80 name = "coverage-4.5.1";
80 name = "coverage-4.5.1";
81 doCheck = false;
81 doCheck = false;
82 src = fetchurl {
82 src = fetchurl {
83 url = "https://files.pythonhosted.org/packages/35/fe/e7df7289d717426093c68d156e0fd9117c8f4872b6588e8a8928a0f68424/coverage-4.5.1.tar.gz";
83 url = "https://files.pythonhosted.org/packages/35/fe/e7df7289d717426093c68d156e0fd9117c8f4872b6588e8a8928a0f68424/coverage-4.5.1.tar.gz";
84 sha256 = "1wbrzpxka3xd4nmmkc6q0ir343d91kymwsm8pbmwa0d2a7q4ir2n";
84 sha256 = "1wbrzpxka3xd4nmmkc6q0ir343d91kymwsm8pbmwa0d2a7q4ir2n";
85 };
85 };
86 meta = {
86 meta = {
87 license = [ pkgs.lib.licenses.asl20 ];
87 license = [ pkgs.lib.licenses.asl20 ];
88 };
88 };
89 };
89 };
90 "decorator" = super.buildPythonPackage {
90 "decorator" = super.buildPythonPackage {
91 name = "decorator-4.1.2";
91 name = "decorator-4.1.2";
92 doCheck = false;
92 doCheck = false;
93 src = fetchurl {
93 src = fetchurl {
94 url = "https://files.pythonhosted.org/packages/bb/e0/f6e41e9091e130bf16d4437dabbac3993908e4d6485ecbc985ef1352db94/decorator-4.1.2.tar.gz";
94 url = "https://files.pythonhosted.org/packages/bb/e0/f6e41e9091e130bf16d4437dabbac3993908e4d6485ecbc985ef1352db94/decorator-4.1.2.tar.gz";
95 sha256 = "1d8npb11kxyi36mrvjdpcjij76l5zfyrz2f820brf0l0rcw4vdkw";
95 sha256 = "1d8npb11kxyi36mrvjdpcjij76l5zfyrz2f820brf0l0rcw4vdkw";
96 };
96 };
97 meta = {
97 meta = {
98 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "new BSD License"; } ];
98 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "new BSD License"; } ];
99 };
99 };
100 };
100 };
101 "dogpile.cache" = super.buildPythonPackage {
101 "dogpile.cache" = super.buildPythonPackage {
102 name = "dogpile.cache-0.6.7";
102 name = "dogpile.cache-0.7.1";
103 doCheck = false;
103 doCheck = false;
104 propagatedBuildInputs = [
105 self."decorator"
106 ];
104 src = fetchurl {
107 src = fetchurl {
105 url = "https://files.pythonhosted.org/packages/ee/bd/440da735a11c6087eed7cc8747fc4b995cbac2464168682f8ee1c8e43844/dogpile.cache-0.6.7.tar.gz";
108 url = "https://files.pythonhosted.org/packages/84/3e/dbf1cfc5228f1d3dca80ef714db2c5aaec5cd9efaf54d7e3daef6bc48b19/dogpile.cache-0.7.1.tar.gz";
106 sha256 = "1aw8rx8vhb75y7zc6gi67g21sw057jdx7i8m3jq7kf3nqavxx9zw";
109 sha256 = "0caazmrzhnfqb5yrp8myhw61ny637jj69wcngrpbvi31jlcpy6v9";
107 };
110 };
108 meta = {
111 meta = {
109 license = [ pkgs.lib.licenses.bsdOriginal ];
112 license = [ pkgs.lib.licenses.bsdOriginal ];
110 };
113 };
111 };
114 };
112 "dogpile.core" = super.buildPythonPackage {
115 "dogpile.core" = super.buildPythonPackage {
113 name = "dogpile.core-0.4.1";
116 name = "dogpile.core-0.4.1";
114 doCheck = false;
117 doCheck = false;
115 src = fetchurl {
118 src = fetchurl {
116 url = "https://files.pythonhosted.org/packages/0e/77/e72abc04c22aedf874301861e5c1e761231c288b5de369c18be8f4b5c9bb/dogpile.core-0.4.1.tar.gz";
119 url = "https://files.pythonhosted.org/packages/0e/77/e72abc04c22aedf874301861e5c1e761231c288b5de369c18be8f4b5c9bb/dogpile.core-0.4.1.tar.gz";
117 sha256 = "0xpdvg4kr1isfkrh1rfsh7za4q5a5s6l2kf9wpvndbwf3aqjyrdy";
120 sha256 = "0xpdvg4kr1isfkrh1rfsh7za4q5a5s6l2kf9wpvndbwf3aqjyrdy";
118 };
121 };
119 meta = {
122 meta = {
120 license = [ pkgs.lib.licenses.bsdOriginal ];
123 license = [ pkgs.lib.licenses.bsdOriginal ];
121 };
124 };
122 };
125 };
123 "dulwich" = super.buildPythonPackage {
126 "dulwich" = super.buildPythonPackage {
124 name = "dulwich-0.13.0";
127 name = "dulwich-0.13.0";
125 doCheck = false;
128 doCheck = false;
126 src = fetchurl {
129 src = fetchurl {
127 url = "https://files.pythonhosted.org/packages/84/95/732d280eee829dacc954e8109f97b47abcadcca472c2ab013e1635eb4792/dulwich-0.13.0.tar.gz";
130 url = "https://files.pythonhosted.org/packages/84/95/732d280eee829dacc954e8109f97b47abcadcca472c2ab013e1635eb4792/dulwich-0.13.0.tar.gz";
128 sha256 = "0f1jwvrh549c4rgavkn3wizrch904s73s4fmrxykxy9cw8s57lwf";
131 sha256 = "0f1jwvrh549c4rgavkn3wizrch904s73s4fmrxykxy9cw8s57lwf";
129 };
132 };
130 meta = {
133 meta = {
131 license = [ pkgs.lib.licenses.gpl2Plus ];
134 license = [ pkgs.lib.licenses.gpl2Plus ];
132 };
135 };
133 };
136 };
134 "enum34" = super.buildPythonPackage {
137 "enum34" = super.buildPythonPackage {
135 name = "enum34-1.1.6";
138 name = "enum34-1.1.6";
136 doCheck = false;
139 doCheck = false;
137 src = fetchurl {
140 src = fetchurl {
138 url = "https://files.pythonhosted.org/packages/bf/3e/31d502c25302814a7c2f1d3959d2a3b3f78e509002ba91aea64993936876/enum34-1.1.6.tar.gz";
141 url = "https://files.pythonhosted.org/packages/bf/3e/31d502c25302814a7c2f1d3959d2a3b3f78e509002ba91aea64993936876/enum34-1.1.6.tar.gz";
139 sha256 = "1cgm5ng2gcfrkrm3hc22brl6chdmv67b9zvva9sfs7gn7dwc9n4a";
142 sha256 = "1cgm5ng2gcfrkrm3hc22brl6chdmv67b9zvva9sfs7gn7dwc9n4a";
140 };
143 };
141 meta = {
144 meta = {
142 license = [ pkgs.lib.licenses.bsdOriginal ];
145 license = [ pkgs.lib.licenses.bsdOriginal ];
143 };
146 };
144 };
147 };
145 "funcsigs" = super.buildPythonPackage {
148 "funcsigs" = super.buildPythonPackage {
146 name = "funcsigs-1.0.2";
149 name = "funcsigs-1.0.2";
147 doCheck = false;
150 doCheck = false;
148 src = fetchurl {
151 src = fetchurl {
149 url = "https://files.pythonhosted.org/packages/94/4a/db842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23/funcsigs-1.0.2.tar.gz";
152 url = "https://files.pythonhosted.org/packages/94/4a/db842e7a0545de1cdb0439bb80e6e42dfe82aaeaadd4072f2263a4fbed23/funcsigs-1.0.2.tar.gz";
150 sha256 = "0l4g5818ffyfmfs1a924811azhjj8ax9xd1cffr1mzd3ycn0zfx7";
153 sha256 = "0l4g5818ffyfmfs1a924811azhjj8ax9xd1cffr1mzd3ycn0zfx7";
151 };
154 };
152 meta = {
155 meta = {
153 license = [ { fullName = "ASL"; } pkgs.lib.licenses.asl20 ];
156 license = [ { fullName = "ASL"; } pkgs.lib.licenses.asl20 ];
154 };
157 };
155 };
158 };
156 "gevent" = super.buildPythonPackage {
159 "gevent" = super.buildPythonPackage {
157 name = "gevent-1.3.7";
160 name = "gevent-1.4.0";
158 doCheck = false;
161 doCheck = false;
159 propagatedBuildInputs = [
162 propagatedBuildInputs = [
160 self."greenlet"
163 self."greenlet"
161 ];
164 ];
162 src = fetchurl {
165 src = fetchurl {
163 url = "https://files.pythonhosted.org/packages/10/c1/9499b146bfa43aa4f1e0ed1bab1bd3209a4861d25650c11725036c731cf5/gevent-1.3.7.tar.gz";
166 url = "https://files.pythonhosted.org/packages/ed/27/6c49b70808f569b66ec7fac2e78f076e9b204db9cf5768740cff3d5a07ae/gevent-1.4.0.tar.gz";
164 sha256 = "0b0fr04qdk1p4sniv87fh8z5psac60x01pv054kpgi94520g81iz";
167 sha256 = "1lchr4akw2jkm5v4kz7bdm4wv3knkfhbfn9vkkz4s5yrkcxzmdqy";
165 };
168 };
166 meta = {
169 meta = {
167 license = [ pkgs.lib.licenses.mit ];
170 license = [ pkgs.lib.licenses.mit ];
168 };
171 };
169 };
172 };
170 "gprof2dot" = super.buildPythonPackage {
173 "gprof2dot" = super.buildPythonPackage {
171 name = "gprof2dot-2017.9.19";
174 name = "gprof2dot-2017.9.19";
172 doCheck = false;
175 doCheck = false;
173 src = fetchurl {
176 src = fetchurl {
174 url = "https://files.pythonhosted.org/packages/9d/36/f977122502979f3dfb50704979c9ed70e6b620787942b089bf1af15f5aba/gprof2dot-2017.9.19.tar.gz";
177 url = "https://files.pythonhosted.org/packages/9d/36/f977122502979f3dfb50704979c9ed70e6b620787942b089bf1af15f5aba/gprof2dot-2017.9.19.tar.gz";
175 sha256 = "17ih23ld2nzgc3xwgbay911l6lh96jp1zshmskm17n1gg2i7mg6f";
178 sha256 = "17ih23ld2nzgc3xwgbay911l6lh96jp1zshmskm17n1gg2i7mg6f";
176 };
179 };
177 meta = {
180 meta = {
178 license = [ { fullName = "GNU Lesser General Public License v3 or later (LGPLv3+)"; } { fullName = "LGPL"; } ];
181 license = [ { fullName = "GNU Lesser General Public License v3 or later (LGPLv3+)"; } { fullName = "LGPL"; } ];
179 };
182 };
180 };
183 };
181 "greenlet" = super.buildPythonPackage {
184 "greenlet" = super.buildPythonPackage {
182 name = "greenlet-0.4.15";
185 name = "greenlet-0.4.15";
183 doCheck = false;
186 doCheck = false;
184 src = fetchurl {
187 src = fetchurl {
185 url = "https://files.pythonhosted.org/packages/f8/e8/b30ae23b45f69aa3f024b46064c0ac8e5fcb4f22ace0dca8d6f9c8bbe5e7/greenlet-0.4.15.tar.gz";
188 url = "https://files.pythonhosted.org/packages/f8/e8/b30ae23b45f69aa3f024b46064c0ac8e5fcb4f22ace0dca8d6f9c8bbe5e7/greenlet-0.4.15.tar.gz";
186 sha256 = "1g4g1wwc472ds89zmqlpyan3fbnzpa8qm48z3z1y6mlk44z485ll";
189 sha256 = "1g4g1wwc472ds89zmqlpyan3fbnzpa8qm48z3z1y6mlk44z485ll";
187 };
190 };
188 meta = {
191 meta = {
189 license = [ pkgs.lib.licenses.mit ];
192 license = [ pkgs.lib.licenses.mit ];
190 };
193 };
191 };
194 };
192 "gunicorn" = super.buildPythonPackage {
195 "gunicorn" = super.buildPythonPackage {
193 name = "gunicorn-19.9.0";
196 name = "gunicorn-19.9.0";
194 doCheck = false;
197 doCheck = false;
195 src = fetchurl {
198 src = fetchurl {
196 url = "https://files.pythonhosted.org/packages/47/52/68ba8e5e8ba251e54006a49441f7ccabca83b6bef5aedacb4890596c7911/gunicorn-19.9.0.tar.gz";
199 url = "https://files.pythonhosted.org/packages/47/52/68ba8e5e8ba251e54006a49441f7ccabca83b6bef5aedacb4890596c7911/gunicorn-19.9.0.tar.gz";
197 sha256 = "1wzlf4xmn6qjirh5w81l6i6kqjnab1n1qqkh7zsj1yb6gh4n49ps";
200 sha256 = "1wzlf4xmn6qjirh5w81l6i6kqjnab1n1qqkh7zsj1yb6gh4n49ps";
198 };
201 };
199 meta = {
202 meta = {
200 license = [ pkgs.lib.licenses.mit ];
203 license = [ pkgs.lib.licenses.mit ];
201 };
204 };
202 };
205 };
203 "hg-evolve" = super.buildPythonPackage {
206 "hg-evolve" = super.buildPythonPackage {
204 name = "hg-evolve-8.0.1";
207 name = "hg-evolve-8.0.1";
205 doCheck = false;
208 doCheck = false;
206 src = fetchurl {
209 src = fetchurl {
207 url = "https://files.pythonhosted.org/packages/06/1a/c5c12d8f117426f05285a820ee5a23121882f5381104e86276b72598934f/hg-evolve-8.0.1.tar.gz";
210 url = "https://files.pythonhosted.org/packages/06/1a/c5c12d8f117426f05285a820ee5a23121882f5381104e86276b72598934f/hg-evolve-8.0.1.tar.gz";
208 sha256 = "1brafifb42k71gl7qssb5m3ijnm7y30lfvm90z8xxcr2fgz19p29";
211 sha256 = "1brafifb42k71gl7qssb5m3ijnm7y30lfvm90z8xxcr2fgz19p29";
209 };
212 };
210 meta = {
213 meta = {
211 license = [ { fullName = "GPLv2+"; } ];
214 license = [ { fullName = "GPLv2+"; } ];
212 };
215 };
213 };
216 };
214 "hgsubversion" = super.buildPythonPackage {
217 "hgsubversion" = super.buildPythonPackage {
215 name = "hgsubversion-1.9.3";
218 name = "hgsubversion-1.9.3";
216 doCheck = false;
219 doCheck = false;
217 propagatedBuildInputs = [
220 propagatedBuildInputs = [
218 self."mercurial"
221 self."mercurial"
219 self."subvertpy"
222 self."subvertpy"
220 ];
223 ];
221 src = fetchurl {
224 src = fetchurl {
222 url = "https://files.pythonhosted.org/packages/a3/53/6d205e641f3e09abcf1ddaed66e5e4b20da22d0145566d440a02c9e35f0d/hgsubversion-1.9.3.tar.gz";
225 url = "https://files.pythonhosted.org/packages/a3/53/6d205e641f3e09abcf1ddaed66e5e4b20da22d0145566d440a02c9e35f0d/hgsubversion-1.9.3.tar.gz";
223 sha256 = "0nymcjlch8c4zjbncrs30p2nrbylsf25g3h6mr0zzzxr141h3sig";
226 sha256 = "0nymcjlch8c4zjbncrs30p2nrbylsf25g3h6mr0zzzxr141h3sig";
224 };
227 };
225 meta = {
228 meta = {
226 license = [ pkgs.lib.licenses.gpl1 ];
229 license = [ pkgs.lib.licenses.gpl1 ];
227 };
230 };
228 };
231 };
229 "hupper" = super.buildPythonPackage {
232 "hupper" = super.buildPythonPackage {
230 name = "hupper-1.4.2";
233 name = "hupper-1.4.2";
231 doCheck = false;
234 doCheck = false;
232 src = fetchurl {
235 src = fetchurl {
233 url = "https://files.pythonhosted.org/packages/f1/75/1915dc7650b4867fa3049256e24ca8eddb5989998fcec788cf52b9812dfc/hupper-1.4.2.tar.gz";
236 url = "https://files.pythonhosted.org/packages/f1/75/1915dc7650b4867fa3049256e24ca8eddb5989998fcec788cf52b9812dfc/hupper-1.4.2.tar.gz";
234 sha256 = "16vb9fkiaakdpcp6pn56h3w0dwvm67bxq2k2dv4i382qhqwphdzb";
237 sha256 = "16vb9fkiaakdpcp6pn56h3w0dwvm67bxq2k2dv4i382qhqwphdzb";
235 };
238 };
236 meta = {
239 meta = {
237 license = [ pkgs.lib.licenses.mit ];
240 license = [ pkgs.lib.licenses.mit ];
238 };
241 };
239 };
242 };
240 "ipdb" = super.buildPythonPackage {
243 "ipdb" = super.buildPythonPackage {
241 name = "ipdb-0.11";
244 name = "ipdb-0.11";
242 doCheck = false;
245 doCheck = false;
243 propagatedBuildInputs = [
246 propagatedBuildInputs = [
244 self."setuptools"
247 self."setuptools"
245 self."ipython"
248 self."ipython"
246 ];
249 ];
247 src = fetchurl {
250 src = fetchurl {
248 url = "https://files.pythonhosted.org/packages/80/fe/4564de08f174f3846364b3add8426d14cebee228f741c27e702b2877e85b/ipdb-0.11.tar.gz";
251 url = "https://files.pythonhosted.org/packages/80/fe/4564de08f174f3846364b3add8426d14cebee228f741c27e702b2877e85b/ipdb-0.11.tar.gz";
249 sha256 = "02m0l8wrhhd3z7dg3czn5ys1g5pxib516hpshdzp7rxzsxgcd0bh";
252 sha256 = "02m0l8wrhhd3z7dg3czn5ys1g5pxib516hpshdzp7rxzsxgcd0bh";
250 };
253 };
251 meta = {
254 meta = {
252 license = [ pkgs.lib.licenses.bsdOriginal ];
255 license = [ pkgs.lib.licenses.bsdOriginal ];
253 };
256 };
254 };
257 };
255 "ipython" = super.buildPythonPackage {
258 "ipython" = super.buildPythonPackage {
256 name = "ipython-5.1.0";
259 name = "ipython-5.1.0";
257 doCheck = false;
260 doCheck = false;
258 propagatedBuildInputs = [
261 propagatedBuildInputs = [
259 self."setuptools"
262 self."setuptools"
260 self."decorator"
263 self."decorator"
261 self."pickleshare"
264 self."pickleshare"
262 self."simplegeneric"
265 self."simplegeneric"
263 self."traitlets"
266 self."traitlets"
264 self."prompt-toolkit"
267 self."prompt-toolkit"
265 self."pygments"
268 self."pygments"
266 self."pexpect"
269 self."pexpect"
267 self."backports.shutil-get-terminal-size"
270 self."backports.shutil-get-terminal-size"
268 self."pathlib2"
271 self."pathlib2"
269 self."pexpect"
272 self."pexpect"
270 ];
273 ];
271 src = fetchurl {
274 src = fetchurl {
272 url = "https://files.pythonhosted.org/packages/89/63/a9292f7cd9d0090a0f995e1167f3f17d5889dcbc9a175261719c513b9848/ipython-5.1.0.tar.gz";
275 url = "https://files.pythonhosted.org/packages/89/63/a9292f7cd9d0090a0f995e1167f3f17d5889dcbc9a175261719c513b9848/ipython-5.1.0.tar.gz";
273 sha256 = "0qdrf6aj9kvjczd5chj1my8y2iq09am9l8bb2a1334a52d76kx3y";
276 sha256 = "0qdrf6aj9kvjczd5chj1my8y2iq09am9l8bb2a1334a52d76kx3y";
274 };
277 };
275 meta = {
278 meta = {
276 license = [ pkgs.lib.licenses.bsdOriginal ];
279 license = [ pkgs.lib.licenses.bsdOriginal ];
277 };
280 };
278 };
281 };
279 "ipython-genutils" = super.buildPythonPackage {
282 "ipython-genutils" = super.buildPythonPackage {
280 name = "ipython-genutils-0.2.0";
283 name = "ipython-genutils-0.2.0";
281 doCheck = false;
284 doCheck = false;
282 src = fetchurl {
285 src = fetchurl {
283 url = "https://files.pythonhosted.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz";
286 url = "https://files.pythonhosted.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz";
284 sha256 = "1a4bc9y8hnvq6cp08qs4mckgm6i6ajpndp4g496rvvzcfmp12bpb";
287 sha256 = "1a4bc9y8hnvq6cp08qs4mckgm6i6ajpndp4g496rvvzcfmp12bpb";
285 };
288 };
286 meta = {
289 meta = {
287 license = [ pkgs.lib.licenses.bsdOriginal ];
290 license = [ pkgs.lib.licenses.bsdOriginal ];
288 };
291 };
289 };
292 };
290 "mako" = super.buildPythonPackage {
293 "mako" = super.buildPythonPackage {
291 name = "mako-1.0.7";
294 name = "mako-1.0.7";
292 doCheck = false;
295 doCheck = false;
293 propagatedBuildInputs = [
296 propagatedBuildInputs = [
294 self."markupsafe"
297 self."markupsafe"
295 ];
298 ];
296 src = fetchurl {
299 src = fetchurl {
297 url = "https://files.pythonhosted.org/packages/eb/f3/67579bb486517c0d49547f9697e36582cd19dafb5df9e687ed8e22de57fa/Mako-1.0.7.tar.gz";
300 url = "https://files.pythonhosted.org/packages/eb/f3/67579bb486517c0d49547f9697e36582cd19dafb5df9e687ed8e22de57fa/Mako-1.0.7.tar.gz";
298 sha256 = "1bi5gnr8r8dva06qpyx4kgjc6spm2k1y908183nbbaylggjzs0jf";
301 sha256 = "1bi5gnr8r8dva06qpyx4kgjc6spm2k1y908183nbbaylggjzs0jf";
299 };
302 };
300 meta = {
303 meta = {
301 license = [ pkgs.lib.licenses.mit ];
304 license = [ pkgs.lib.licenses.mit ];
302 };
305 };
303 };
306 };
304 "markupsafe" = super.buildPythonPackage {
307 "markupsafe" = super.buildPythonPackage {
305 name = "markupsafe-1.0";
308 name = "markupsafe-1.1.0";
306 doCheck = false;
309 doCheck = false;
307 src = fetchurl {
310 src = fetchurl {
308 url = "https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz";
311 url = "https://files.pythonhosted.org/packages/ac/7e/1b4c2e05809a4414ebce0892fe1e32c14ace86ca7d50c70f00979ca9b3a3/MarkupSafe-1.1.0.tar.gz";
309 sha256 = "0rdn1s8x9ni7ss8rfiacj7x1085lx8mh2zdwqslnw8xc3l4nkgm6";
312 sha256 = "1lxirjypbdd3l9jl4vliilhfnhy7c7f2vlldqg1b0i74khn375sf";
310 };
313 };
311 meta = {
314 meta = {
312 license = [ pkgs.lib.licenses.bsdOriginal ];
315 license = [ pkgs.lib.licenses.bsdOriginal ];
313 };
316 };
314 };
317 };
315 "mercurial" = super.buildPythonPackage {
318 "mercurial" = super.buildPythonPackage {
316 name = "mercurial-4.6.2";
319 name = "mercurial-4.6.2";
317 doCheck = false;
320 doCheck = false;
318 src = fetchurl {
321 src = fetchurl {
319 url = "https://files.pythonhosted.org/packages/d9/fb/c7ecf2b7fd349878dbf45b8390b8db735cef73d49dd9ce8a364b4ca3a846/mercurial-4.6.2.tar.gz";
322 url = "https://files.pythonhosted.org/packages/d9/fb/c7ecf2b7fd349878dbf45b8390b8db735cef73d49dd9ce8a364b4ca3a846/mercurial-4.6.2.tar.gz";
320 sha256 = "1bv6wgcdx8glihjjfg22khhc52mclsn4kwfqvzbzlg0b42h4xl0w";
323 sha256 = "1bv6wgcdx8glihjjfg22khhc52mclsn4kwfqvzbzlg0b42h4xl0w";
321 };
324 };
322 meta = {
325 meta = {
323 license = [ pkgs.lib.licenses.gpl1 pkgs.lib.licenses.gpl2Plus ];
326 license = [ pkgs.lib.licenses.gpl1 pkgs.lib.licenses.gpl2Plus ];
324 };
327 };
325 };
328 };
326 "mock" = super.buildPythonPackage {
329 "mock" = super.buildPythonPackage {
327 name = "mock-1.0.1";
330 name = "mock-1.0.1";
328 doCheck = false;
331 doCheck = false;
329 src = fetchurl {
332 src = fetchurl {
330 url = "https://files.pythonhosted.org/packages/a2/52/7edcd94f0afb721a2d559a5b9aae8af4f8f2c79bc63fdbe8a8a6c9b23bbe/mock-1.0.1.tar.gz";
333 url = "https://files.pythonhosted.org/packages/a2/52/7edcd94f0afb721a2d559a5b9aae8af4f8f2c79bc63fdbe8a8a6c9b23bbe/mock-1.0.1.tar.gz";
331 sha256 = "0kzlsbki6q0awf89rc287f3aj8x431lrajf160a70z0ikhnxsfdq";
334 sha256 = "0kzlsbki6q0awf89rc287f3aj8x431lrajf160a70z0ikhnxsfdq";
332 };
335 };
333 meta = {
336 meta = {
334 license = [ pkgs.lib.licenses.bsdOriginal ];
337 license = [ pkgs.lib.licenses.bsdOriginal ];
335 };
338 };
336 };
339 };
337 "more-itertools" = super.buildPythonPackage {
340 "more-itertools" = super.buildPythonPackage {
338 name = "more-itertools-4.3.0";
341 name = "more-itertools-5.0.0";
339 doCheck = false;
342 doCheck = false;
340 propagatedBuildInputs = [
343 propagatedBuildInputs = [
341 self."six"
344 self."six"
342 ];
345 ];
343 src = fetchurl {
346 src = fetchurl {
344 url = "https://files.pythonhosted.org/packages/88/ff/6d485d7362f39880810278bdc906c13300db05485d9c65971dec1142da6a/more-itertools-4.3.0.tar.gz";
347 url = "https://files.pythonhosted.org/packages/dd/26/30fc0d541d9fdf55faf5ba4b0fd68f81d5bd2447579224820ad525934178/more-itertools-5.0.0.tar.gz";
345 sha256 = "17h3na0rdh8xq30w4b9pizgkdxmm51896bxw600x84jflg9vaxn4";
348 sha256 = "1r12cm6mcdwdzz7d47a6g4l437xsvapdlgyhqay3i2nrlv03da9q";
346 };
349 };
347 meta = {
350 meta = {
348 license = [ pkgs.lib.licenses.mit ];
351 license = [ pkgs.lib.licenses.mit ];
349 };
352 };
350 };
353 };
351 "msgpack-python" = super.buildPythonPackage {
354 "msgpack-python" = super.buildPythonPackage {
352 name = "msgpack-python-0.5.6";
355 name = "msgpack-python-0.5.6";
353 doCheck = false;
356 doCheck = false;
354 src = fetchurl {
357 src = fetchurl {
355 url = "https://files.pythonhosted.org/packages/8a/20/6eca772d1a5830336f84aca1d8198e5a3f4715cd1c7fc36d3cc7f7185091/msgpack-python-0.5.6.tar.gz";
358 url = "https://files.pythonhosted.org/packages/8a/20/6eca772d1a5830336f84aca1d8198e5a3f4715cd1c7fc36d3cc7f7185091/msgpack-python-0.5.6.tar.gz";
356 sha256 = "16wh8qgybmfh4pjp8vfv78mdlkxfmcasg78lzlnm6nslsfkci31p";
359 sha256 = "16wh8qgybmfh4pjp8vfv78mdlkxfmcasg78lzlnm6nslsfkci31p";
357 };
360 };
358 meta = {
361 meta = {
359 license = [ pkgs.lib.licenses.asl20 ];
362 license = [ pkgs.lib.licenses.asl20 ];
360 };
363 };
361 };
364 };
362 "pastedeploy" = super.buildPythonPackage {
365 "pastedeploy" = super.buildPythonPackage {
363 name = "pastedeploy-1.5.2";
366 name = "pastedeploy-2.0.1";
364 doCheck = false;
367 doCheck = false;
365 src = fetchurl {
368 src = fetchurl {
366 url = "https://files.pythonhosted.org/packages/0f/90/8e20cdae206c543ea10793cbf4136eb9a8b3f417e04e40a29d72d9922cbd/PasteDeploy-1.5.2.tar.gz";
369 url = "https://files.pythonhosted.org/packages/19/a0/5623701df7e2478a68a1b685d1a84518024eef994cde7e4da8449a31616f/PasteDeploy-2.0.1.tar.gz";
367 sha256 = "1jz3m4hq8v6hyhfjz9425nd3nvn52cvbfipdcd72krjmla4qz1fm";
370 sha256 = "02imfbbx1mi2h546f3sr37m47dk9qizaqhzzlhx8bkzxa6fzn8yl";
368 };
371 };
369 meta = {
372 meta = {
370 license = [ pkgs.lib.licenses.mit ];
373 license = [ pkgs.lib.licenses.mit ];
371 };
374 };
372 };
375 };
373 "pathlib2" = super.buildPythonPackage {
376 "pathlib2" = super.buildPythonPackage {
374 name = "pathlib2-2.3.2";
377 name = "pathlib2-2.3.3";
375 doCheck = false;
378 doCheck = false;
376 propagatedBuildInputs = [
379 propagatedBuildInputs = [
377 self."six"
380 self."six"
378 self."scandir"
381 self."scandir"
379 ];
382 ];
380 src = fetchurl {
383 src = fetchurl {
381 url = "https://files.pythonhosted.org/packages/db/a8/7d6439c1aec525ed70810abee5b7d7f3aa35347f59bc28343e8f62019aa2/pathlib2-2.3.2.tar.gz";
384 url = "https://files.pythonhosted.org/packages/bf/d7/a2568f4596b75d2c6e2b4094a7e64f620decc7887f69a1f2811931ea15b9/pathlib2-2.3.3.tar.gz";
382 sha256 = "10yb0iv5x2hs631rcppkhbddx799d3h8pcwmkbh2a66ns3w71ccf";
385 sha256 = "0hpp92vqqgcd8h92msm9slv161b1q160igjwnkf2ag6cx0c96695";
383 };
386 };
384 meta = {
387 meta = {
385 license = [ pkgs.lib.licenses.mit ];
388 license = [ pkgs.lib.licenses.mit ];
386 };
389 };
387 };
390 };
388 "pexpect" = super.buildPythonPackage {
391 "pexpect" = super.buildPythonPackage {
389 name = "pexpect-4.6.0";
392 name = "pexpect-4.6.0";
390 doCheck = false;
393 doCheck = false;
391 propagatedBuildInputs = [
394 propagatedBuildInputs = [
392 self."ptyprocess"
395 self."ptyprocess"
393 ];
396 ];
394 src = fetchurl {
397 src = fetchurl {
395 url = "https://files.pythonhosted.org/packages/89/43/07d07654ee3e25235d8cea4164cdee0ec39d1fda8e9203156ebe403ffda4/pexpect-4.6.0.tar.gz";
398 url = "https://files.pythonhosted.org/packages/89/43/07d07654ee3e25235d8cea4164cdee0ec39d1fda8e9203156ebe403ffda4/pexpect-4.6.0.tar.gz";
396 sha256 = "1fla85g47iaxxpjhp9vkxdnv4pgc7rplfy6ja491smrrk0jqi3ia";
399 sha256 = "1fla85g47iaxxpjhp9vkxdnv4pgc7rplfy6ja491smrrk0jqi3ia";
397 };
400 };
398 meta = {
401 meta = {
399 license = [ pkgs.lib.licenses.isc { fullName = "ISC License (ISCL)"; } ];
402 license = [ pkgs.lib.licenses.isc { fullName = "ISC License (ISCL)"; } ];
400 };
403 };
401 };
404 };
402 "pickleshare" = super.buildPythonPackage {
405 "pickleshare" = super.buildPythonPackage {
403 name = "pickleshare-0.7.5";
406 name = "pickleshare-0.7.5";
404 doCheck = false;
407 doCheck = false;
405 propagatedBuildInputs = [
408 propagatedBuildInputs = [
406 self."pathlib2"
409 self."pathlib2"
407 ];
410 ];
408 src = fetchurl {
411 src = fetchurl {
409 url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz";
412 url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz";
410 sha256 = "1jmghg3c53yp1i8cm6pcrm280ayi8621rwyav9fac7awjr3kss47";
413 sha256 = "1jmghg3c53yp1i8cm6pcrm280ayi8621rwyav9fac7awjr3kss47";
411 };
414 };
412 meta = {
415 meta = {
413 license = [ pkgs.lib.licenses.mit ];
416 license = [ pkgs.lib.licenses.mit ];
414 };
417 };
415 };
418 };
416 "plaster" = super.buildPythonPackage {
419 "plaster" = super.buildPythonPackage {
417 name = "plaster-1.0";
420 name = "plaster-1.0";
418 doCheck = false;
421 doCheck = false;
419 propagatedBuildInputs = [
422 propagatedBuildInputs = [
420 self."setuptools"
423 self."setuptools"
421 ];
424 ];
422 src = fetchurl {
425 src = fetchurl {
423 url = "https://files.pythonhosted.org/packages/37/e1/56d04382d718d32751017d32f351214384e529b794084eee20bb52405563/plaster-1.0.tar.gz";
426 url = "https://files.pythonhosted.org/packages/37/e1/56d04382d718d32751017d32f351214384e529b794084eee20bb52405563/plaster-1.0.tar.gz";
424 sha256 = "1hy8k0nv2mxq94y5aysk6hjk9ryb4bsd13g83m60hcyzxz3wflc3";
427 sha256 = "1hy8k0nv2mxq94y5aysk6hjk9ryb4bsd13g83m60hcyzxz3wflc3";
425 };
428 };
426 meta = {
429 meta = {
427 license = [ pkgs.lib.licenses.mit ];
430 license = [ pkgs.lib.licenses.mit ];
428 };
431 };
429 };
432 };
430 "plaster-pastedeploy" = super.buildPythonPackage {
433 "plaster-pastedeploy" = super.buildPythonPackage {
431 name = "plaster-pastedeploy-0.6";
434 name = "plaster-pastedeploy-0.6";
432 doCheck = false;
435 doCheck = false;
433 propagatedBuildInputs = [
436 propagatedBuildInputs = [
434 self."pastedeploy"
437 self."pastedeploy"
435 self."plaster"
438 self."plaster"
436 ];
439 ];
437 src = fetchurl {
440 src = fetchurl {
438 url = "https://files.pythonhosted.org/packages/3f/e7/6a6833158d2038ec40085433308a1e164fd1dac595513f6dd556d5669bb8/plaster_pastedeploy-0.6.tar.gz";
441 url = "https://files.pythonhosted.org/packages/3f/e7/6a6833158d2038ec40085433308a1e164fd1dac595513f6dd556d5669bb8/plaster_pastedeploy-0.6.tar.gz";
439 sha256 = "1bkggk18f4z2bmsmxyxabvf62znvjwbivzh880419r3ap0616cf2";
442 sha256 = "1bkggk18f4z2bmsmxyxabvf62znvjwbivzh880419r3ap0616cf2";
440 };
443 };
441 meta = {
444 meta = {
442 license = [ pkgs.lib.licenses.mit ];
445 license = [ pkgs.lib.licenses.mit ];
443 };
446 };
444 };
447 };
445 "pluggy" = super.buildPythonPackage {
448 "pluggy" = super.buildPythonPackage {
446 name = "pluggy-0.8.0";
449 name = "pluggy-0.8.1";
447 doCheck = false;
450 doCheck = false;
448 src = fetchurl {
451 src = fetchurl {
449 url = "https://files.pythonhosted.org/packages/65/25/81d0de17cd00f8ca994a4e74e3c4baf7cd25072c0b831dad5c7d9d6138f8/pluggy-0.8.0.tar.gz";
452 url = "https://files.pythonhosted.org/packages/38/e1/83b10c17688af7b2998fa5342fec58ecbd2a5a7499f31e606ae6640b71ac/pluggy-0.8.1.tar.gz";
450 sha256 = "1580p47l2zqzsza8jcnw1h2wh3vvmygk6ly8bvi4w0g8j14sjys4";
453 sha256 = "05l6g42p9ilmabw0hlbiyxy6gyzjri41m5l11a8dzgvi77q35p4d";
451 };
454 };
452 meta = {
455 meta = {
453 license = [ pkgs.lib.licenses.mit ];
456 license = [ pkgs.lib.licenses.mit ];
454 };
457 };
455 };
458 };
456 "prompt-toolkit" = super.buildPythonPackage {
459 "prompt-toolkit" = super.buildPythonPackage {
457 name = "prompt-toolkit-1.0.15";
460 name = "prompt-toolkit-1.0.15";
458 doCheck = false;
461 doCheck = false;
459 propagatedBuildInputs = [
462 propagatedBuildInputs = [
460 self."six"
463 self."six"
461 self."wcwidth"
464 self."wcwidth"
462 ];
465 ];
463 src = fetchurl {
466 src = fetchurl {
464 url = "https://files.pythonhosted.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz";
467 url = "https://files.pythonhosted.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz";
465 sha256 = "05v9h5nydljwpj5nm8n804ms0glajwfy1zagrzqrg91wk3qqi1c5";
468 sha256 = "05v9h5nydljwpj5nm8n804ms0glajwfy1zagrzqrg91wk3qqi1c5";
466 };
469 };
467 meta = {
470 meta = {
468 license = [ pkgs.lib.licenses.bsdOriginal ];
471 license = [ pkgs.lib.licenses.bsdOriginal ];
469 };
472 };
470 };
473 };
471 "psutil" = super.buildPythonPackage {
474 "psutil" = super.buildPythonPackage {
472 name = "psutil-5.4.7";
475 name = "psutil-5.4.8";
473 doCheck = false;
476 doCheck = false;
474 src = fetchurl {
477 src = fetchurl {
475 url = "https://files.pythonhosted.org/packages/7d/9a/1e93d41708f8ed2b564395edfa3389f0fd6d567597401c2e5e2775118d8b/psutil-5.4.7.tar.gz";
478 url = "https://files.pythonhosted.org/packages/e3/58/0eae6e4466e5abf779d7e2b71fac7fba5f59e00ea36ddb3ed690419ccb0f/psutil-5.4.8.tar.gz";
476 sha256 = "0fsgmvzwbdbszkwfnqhib8jcxm4w6zyhvlxlcda0rfm5cyqj4qsv";
479 sha256 = "1hyna338sml2cl1mfb2gs89np18z27mvyhmq4ifh22x07n7mq9kf";
477 };
480 };
478 meta = {
481 meta = {
479 license = [ pkgs.lib.licenses.bsdOriginal ];
482 license = [ pkgs.lib.licenses.bsdOriginal ];
480 };
483 };
481 };
484 };
482 "ptyprocess" = super.buildPythonPackage {
485 "ptyprocess" = super.buildPythonPackage {
483 name = "ptyprocess-0.6.0";
486 name = "ptyprocess-0.6.0";
484 doCheck = false;
487 doCheck = false;
485 src = fetchurl {
488 src = fetchurl {
486 url = "https://files.pythonhosted.org/packages/7d/2d/e4b8733cf79b7309d84c9081a4ab558c89d8c89da5961bf4ddb050ca1ce0/ptyprocess-0.6.0.tar.gz";
489 url = "https://files.pythonhosted.org/packages/7d/2d/e4b8733cf79b7309d84c9081a4ab558c89d8c89da5961bf4ddb050ca1ce0/ptyprocess-0.6.0.tar.gz";
487 sha256 = "1h4lcd3w5nrxnsk436ar7fwkiy5rfn5wj2xwy9l0r4mdqnf2jgwj";
490 sha256 = "1h4lcd3w5nrxnsk436ar7fwkiy5rfn5wj2xwy9l0r4mdqnf2jgwj";
488 };
491 };
489 meta = {
492 meta = {
490 license = [ ];
493 license = [ ];
491 };
494 };
492 };
495 };
493 "py" = super.buildPythonPackage {
496 "py" = super.buildPythonPackage {
494 name = "py-1.6.0";
497 name = "py-1.6.0";
495 doCheck = false;
498 doCheck = false;
496 src = fetchurl {
499 src = fetchurl {
497 url = "https://files.pythonhosted.org/packages/4f/38/5f427d1eedae73063ce4da680d2bae72014995f9fdeaa57809df61c968cd/py-1.6.0.tar.gz";
500 url = "https://files.pythonhosted.org/packages/4f/38/5f427d1eedae73063ce4da680d2bae72014995f9fdeaa57809df61c968cd/py-1.6.0.tar.gz";
498 sha256 = "1wcs3zv9wl5m5x7p16avqj2gsrviyb23yvc3pr330isqs0sh98q6";
501 sha256 = "1wcs3zv9wl5m5x7p16avqj2gsrviyb23yvc3pr330isqs0sh98q6";
499 };
502 };
500 meta = {
503 meta = {
501 license = [ pkgs.lib.licenses.mit ];
504 license = [ pkgs.lib.licenses.mit ];
502 };
505 };
503 };
506 };
504 "pygments" = super.buildPythonPackage {
507 "pygments" = super.buildPythonPackage {
505 name = "pygments-2.3.0";
508 name = "pygments-2.3.1";
506 doCheck = false;
509 doCheck = false;
507 src = fetchurl {
510 src = fetchurl {
508 url = "https://files.pythonhosted.org/packages/63/a2/91c31c4831853dedca2a08a0f94d788fc26a48f7281c99a303769ad2721b/Pygments-2.3.0.tar.gz";
511 url = "https://files.pythonhosted.org/packages/64/69/413708eaf3a64a6abb8972644e0f20891a55e621c6759e2c3f3891e05d63/Pygments-2.3.1.tar.gz";
509 sha256 = "1z34ms51dh4jq4h3cizp7vd1dmsxcbvffkjsd2xxfav22nn6lrl2";
512 sha256 = "0ji87g09jph8jqcvclgb02qvxasdnr9pzvk90rl66d90yqcxmyjz";
510 };
513 };
511 meta = {
514 meta = {
512 license = [ pkgs.lib.licenses.bsdOriginal ];
515 license = [ pkgs.lib.licenses.bsdOriginal ];
513 };
516 };
514 };
517 };
515 "pyramid" = super.buildPythonPackage {
518 "pyramid" = super.buildPythonPackage {
516 name = "pyramid-1.9.2";
519 name = "pyramid-1.10.1";
517 doCheck = false;
520 doCheck = false;
518 propagatedBuildInputs = [
521 propagatedBuildInputs = [
519 self."setuptools"
522 self."hupper"
520 self."webob"
521 self."repoze.lru"
522 self."zope.interface"
523 self."zope.deprecation"
524 self."venusian"
525 self."translationstring"
526 self."pastedeploy"
527 self."plaster"
523 self."plaster"
528 self."plaster-pastedeploy"
524 self."plaster-pastedeploy"
529 self."hupper"
525 self."setuptools"
526 self."translationstring"
527 self."venusian"
528 self."webob"
529 self."zope.deprecation"
530 self."zope.interface"
531 self."repoze.lru"
530 ];
532 ];
531 src = fetchurl {
533 src = fetchurl {
532 url = "https://files.pythonhosted.org/packages/a0/c1/b321d07cfc4870541989ad131c86a1d593bfe802af0eca9718a0dadfb97a/pyramid-1.9.2.tar.gz";
534 url = "https://files.pythonhosted.org/packages/0a/3e/22e3ac9be1b70a01139adba8906ee4b8f628bb469fea3c52f6c97b73063c/pyramid-1.10.1.tar.gz";
533 sha256 = "09drsl0346nchgxp2j7sa5hlk7mkhfld9wvbd0wicacrp26a92fg";
535 sha256 = "1h5105nfh6rsrfjiyw20aavyibj36la3hajy6vh1fa77xb4y3hrp";
534 };
536 };
535 meta = {
537 meta = {
536 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
538 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
537 };
539 };
538 };
540 };
539 "pyramid-mako" = super.buildPythonPackage {
541 "pyramid-mako" = super.buildPythonPackage {
540 name = "pyramid-mako-1.0.2";
542 name = "pyramid-mako-1.0.2";
541 doCheck = false;
543 doCheck = false;
542 propagatedBuildInputs = [
544 propagatedBuildInputs = [
543 self."pyramid"
545 self."pyramid"
544 self."mako"
546 self."mako"
545 ];
547 ];
546 src = fetchurl {
548 src = fetchurl {
547 url = "https://files.pythonhosted.org/packages/f1/92/7e69bcf09676d286a71cb3bbb887b16595b96f9ba7adbdc239ffdd4b1eb9/pyramid_mako-1.0.2.tar.gz";
549 url = "https://files.pythonhosted.org/packages/f1/92/7e69bcf09676d286a71cb3bbb887b16595b96f9ba7adbdc239ffdd4b1eb9/pyramid_mako-1.0.2.tar.gz";
548 sha256 = "18gk2vliq8z4acblsl6yzgbvnr9rlxjlcqir47km7kvlk1xri83d";
550 sha256 = "18gk2vliq8z4acblsl6yzgbvnr9rlxjlcqir47km7kvlk1xri83d";
549 };
551 };
550 meta = {
552 meta = {
551 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
553 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
552 };
554 };
553 };
555 };
554 "pytest" = super.buildPythonPackage {
556 "pytest" = super.buildPythonPackage {
555 name = "pytest-3.8.2";
557 name = "pytest-3.8.2";
556 doCheck = false;
558 doCheck = false;
557 propagatedBuildInputs = [
559 propagatedBuildInputs = [
558 self."py"
560 self."py"
559 self."six"
561 self."six"
560 self."setuptools"
562 self."setuptools"
561 self."attrs"
563 self."attrs"
562 self."more-itertools"
564 self."more-itertools"
563 self."atomicwrites"
565 self."atomicwrites"
564 self."pluggy"
566 self."pluggy"
565 self."funcsigs"
567 self."funcsigs"
566 self."pathlib2"
568 self."pathlib2"
567 ];
569 ];
568 src = fetchurl {
570 src = fetchurl {
569 url = "https://files.pythonhosted.org/packages/5f/d2/7f77f406ac505abda02ab4afb50d06ebf304f6ea42fca34f8f37529106b2/pytest-3.8.2.tar.gz";
571 url = "https://files.pythonhosted.org/packages/5f/d2/7f77f406ac505abda02ab4afb50d06ebf304f6ea42fca34f8f37529106b2/pytest-3.8.2.tar.gz";
570 sha256 = "18nrwzn61kph2y6gxwfz9ms68rfvr9d4vcffsxng9p7jk9z18clk";
572 sha256 = "18nrwzn61kph2y6gxwfz9ms68rfvr9d4vcffsxng9p7jk9z18clk";
571 };
573 };
572 meta = {
574 meta = {
573 license = [ pkgs.lib.licenses.mit ];
575 license = [ pkgs.lib.licenses.mit ];
574 };
576 };
575 };
577 };
576 "pytest-cov" = super.buildPythonPackage {
578 "pytest-cov" = super.buildPythonPackage {
577 name = "pytest-cov-2.6.0";
579 name = "pytest-cov-2.6.0";
578 doCheck = false;
580 doCheck = false;
579 propagatedBuildInputs = [
581 propagatedBuildInputs = [
580 self."pytest"
582 self."pytest"
581 self."coverage"
583 self."coverage"
582 ];
584 ];
583 src = fetchurl {
585 src = fetchurl {
584 url = "https://files.pythonhosted.org/packages/d9/e2/58f90a316fbd94dd50bf5c826a23f3f5d079fb3cc448c1e9f0e3c33a3d2a/pytest-cov-2.6.0.tar.gz";
586 url = "https://files.pythonhosted.org/packages/d9/e2/58f90a316fbd94dd50bf5c826a23f3f5d079fb3cc448c1e9f0e3c33a3d2a/pytest-cov-2.6.0.tar.gz";
585 sha256 = "0qnpp9y3ygx4jk4pf5ad71fh2skbvnr6gl54m7rg5qysnx4g0q73";
587 sha256 = "0qnpp9y3ygx4jk4pf5ad71fh2skbvnr6gl54m7rg5qysnx4g0q73";
586 };
588 };
587 meta = {
589 meta = {
588 license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.mit ];
590 license = [ pkgs.lib.licenses.bsdOriginal pkgs.lib.licenses.mit ];
589 };
591 };
590 };
592 };
591 "pytest-profiling" = super.buildPythonPackage {
593 "pytest-profiling" = super.buildPythonPackage {
592 name = "pytest-profiling-1.3.0";
594 name = "pytest-profiling-1.3.0";
593 doCheck = false;
595 doCheck = false;
594 propagatedBuildInputs = [
596 propagatedBuildInputs = [
595 self."six"
597 self."six"
596 self."pytest"
598 self."pytest"
597 self."gprof2dot"
599 self."gprof2dot"
598 ];
600 ];
599 src = fetchurl {
601 src = fetchurl {
600 url = "https://files.pythonhosted.org/packages/f5/34/4626126e041a51ef50a80d0619519b18d20aef249aac25b0d0fdd47e57ee/pytest-profiling-1.3.0.tar.gz";
602 url = "https://files.pythonhosted.org/packages/f5/34/4626126e041a51ef50a80d0619519b18d20aef249aac25b0d0fdd47e57ee/pytest-profiling-1.3.0.tar.gz";
601 sha256 = "08r5afx5z22yvpmsnl91l4amsy1yxn8qsmm61mhp06mz8zjs51kb";
603 sha256 = "08r5afx5z22yvpmsnl91l4amsy1yxn8qsmm61mhp06mz8zjs51kb";
602 };
604 };
603 meta = {
605 meta = {
604 license = [ pkgs.lib.licenses.mit ];
606 license = [ pkgs.lib.licenses.mit ];
605 };
607 };
606 };
608 };
607 "pytest-runner" = super.buildPythonPackage {
609 "pytest-runner" = super.buildPythonPackage {
608 name = "pytest-runner-4.2";
610 name = "pytest-runner-4.2";
609 doCheck = false;
611 doCheck = false;
610 src = fetchurl {
612 src = fetchurl {
611 url = "https://files.pythonhosted.org/packages/9e/b7/fe6e8f87f9a756fd06722216f1b6698ccba4d269eac6329d9f0c441d0f93/pytest-runner-4.2.tar.gz";
613 url = "https://files.pythonhosted.org/packages/9e/b7/fe6e8f87f9a756fd06722216f1b6698ccba4d269eac6329d9f0c441d0f93/pytest-runner-4.2.tar.gz";
612 sha256 = "1gkpyphawxz38ni1gdq1fmwyqcg02m7ypzqvv46z06crwdxi2gyj";
614 sha256 = "1gkpyphawxz38ni1gdq1fmwyqcg02m7ypzqvv46z06crwdxi2gyj";
613 };
615 };
614 meta = {
616 meta = {
615 license = [ pkgs.lib.licenses.mit ];
617 license = [ pkgs.lib.licenses.mit ];
616 };
618 };
617 };
619 };
618 "pytest-sugar" = super.buildPythonPackage {
620 "pytest-sugar" = super.buildPythonPackage {
619 name = "pytest-sugar-0.9.1";
621 name = "pytest-sugar-0.9.1";
620 doCheck = false;
622 doCheck = false;
621 propagatedBuildInputs = [
623 propagatedBuildInputs = [
622 self."pytest"
624 self."pytest"
623 self."termcolor"
625 self."termcolor"
624 ];
626 ];
625 src = fetchurl {
627 src = fetchurl {
626 url = "https://files.pythonhosted.org/packages/3e/6a/a3f909083079d03bde11d06ab23088886bbe25f2c97fbe4bb865e2bf05bc/pytest-sugar-0.9.1.tar.gz";
628 url = "https://files.pythonhosted.org/packages/3e/6a/a3f909083079d03bde11d06ab23088886bbe25f2c97fbe4bb865e2bf05bc/pytest-sugar-0.9.1.tar.gz";
627 sha256 = "0b4av40dv30727m54v211r0nzwjp2ajkjgxix6j484qjmwpw935b";
629 sha256 = "0b4av40dv30727m54v211r0nzwjp2ajkjgxix6j484qjmwpw935b";
628 };
630 };
629 meta = {
631 meta = {
630 license = [ pkgs.lib.licenses.bsdOriginal ];
632 license = [ pkgs.lib.licenses.bsdOriginal ];
631 };
633 };
632 };
634 };
633 "pytest-timeout" = super.buildPythonPackage {
635 "pytest-timeout" = super.buildPythonPackage {
634 name = "pytest-timeout-1.3.2";
636 name = "pytest-timeout-1.3.2";
635 doCheck = false;
637 doCheck = false;
636 propagatedBuildInputs = [
638 propagatedBuildInputs = [
637 self."pytest"
639 self."pytest"
638 ];
640 ];
639 src = fetchurl {
641 src = fetchurl {
640 url = "https://files.pythonhosted.org/packages/8c/3e/1b6a319d12ae7baa3acb7c18ff2c8630a09471a0319d43535c683b4d03eb/pytest-timeout-1.3.2.tar.gz";
642 url = "https://files.pythonhosted.org/packages/8c/3e/1b6a319d12ae7baa3acb7c18ff2c8630a09471a0319d43535c683b4d03eb/pytest-timeout-1.3.2.tar.gz";
641 sha256 = "09wnmzvnls2mnsdz7x3c3sk2zdp6jl4dryvyj5i8hqz16q2zq5qi";
643 sha256 = "09wnmzvnls2mnsdz7x3c3sk2zdp6jl4dryvyj5i8hqz16q2zq5qi";
642 };
644 };
643 meta = {
645 meta = {
644 license = [ pkgs.lib.licenses.mit { fullName = "DFSG approved"; } ];
646 license = [ pkgs.lib.licenses.mit { fullName = "DFSG approved"; } ];
645 };
647 };
646 };
648 };
647 "repoze.lru" = super.buildPythonPackage {
649 "repoze.lru" = super.buildPythonPackage {
648 name = "repoze.lru-0.7";
650 name = "repoze.lru-0.7";
649 doCheck = false;
651 doCheck = false;
650 src = fetchurl {
652 src = fetchurl {
651 url = "https://files.pythonhosted.org/packages/12/bc/595a77c4b5e204847fdf19268314ef59c85193a9dc9f83630fc459c0fee5/repoze.lru-0.7.tar.gz";
653 url = "https://files.pythonhosted.org/packages/12/bc/595a77c4b5e204847fdf19268314ef59c85193a9dc9f83630fc459c0fee5/repoze.lru-0.7.tar.gz";
652 sha256 = "0xzz1aw2smy8hdszrq8yhnklx6w1r1mf55061kalw3iq35gafa84";
654 sha256 = "0xzz1aw2smy8hdszrq8yhnklx6w1r1mf55061kalw3iq35gafa84";
653 };
655 };
654 meta = {
656 meta = {
655 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
657 license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
656 };
658 };
657 };
659 };
658 "rhodecode-vcsserver" = super.buildPythonPackage {
660 "rhodecode-vcsserver" = super.buildPythonPackage {
659 name = "rhodecode-vcsserver-4.15.2";
661 name = "rhodecode-vcsserver-4.16.0";
660 buildInputs = [
662 buildInputs = [
661 self."pytest"
663 self."pytest"
662 self."py"
664 self."py"
663 self."pytest-cov"
665 self."pytest-cov"
664 self."pytest-sugar"
666 self."pytest-sugar"
665 self."pytest-runner"
667 self."pytest-runner"
666 self."pytest-profiling"
668 self."pytest-profiling"
667 self."pytest-timeout"
669 self."pytest-timeout"
668 self."gprof2dot"
670 self."gprof2dot"
669 self."mock"
671 self."mock"
670 self."webtest"
671 self."cov-core"
672 self."cov-core"
672 self."coverage"
673 self."coverage"
674 self."webtest"
675 self."beautifulsoup4"
673 self."configobj"
676 self."configobj"
674 ];
677 ];
675 doCheck = true;
678 doCheck = true;
676 propagatedBuildInputs = [
679 propagatedBuildInputs = [
677 self."configobj"
680 self."configobj"
678 self."atomicwrites"
681 self."atomicwrites"
679 self."attrs"
682 self."attrs"
680 self."dogpile.cache"
683 self."dogpile.cache"
681 self."dogpile.core"
684 self."dogpile.core"
682 self."decorator"
685 self."decorator"
683 self."dulwich"
686 self."dulwich"
684 self."hgsubversion"
687 self."hgsubversion"
685 self."hg-evolve"
688 self."hg-evolve"
686 self."mako"
689 self."mako"
687 self."markupsafe"
690 self."markupsafe"
688 self."mercurial"
691 self."mercurial"
689 self."msgpack-python"
692 self."msgpack-python"
690 self."pastedeploy"
693 self."pastedeploy"
691 self."psutil"
694 self."psutil"
692 self."pyramid"
695 self."pyramid"
693 self."pyramid-mako"
696 self."pyramid-mako"
694 self."pygments"
697 self."pygments"
695 self."pathlib2"
698 self."pathlib2"
696 self."repoze.lru"
699 self."repoze.lru"
697 self."simplejson"
700 self."simplejson"
698 self."subprocess32"
701 self."subprocess32"
699 self."subvertpy"
702 self."subvertpy"
700 self."six"
703 self."six"
701 self."translationstring"
704 self."translationstring"
702 self."webob"
705 self."webob"
703 self."zope.deprecation"
706 self."zope.deprecation"
704 self."zope.interface"
707 self."zope.interface"
708 self."venusian"
705 self."gevent"
709 self."gevent"
706 self."greenlet"
710 self."greenlet"
707 self."gunicorn"
711 self."gunicorn"
708 self."waitress"
712 self."waitress"
709 self."setproctitle"
713 self."setproctitle"
710 self."ipdb"
714 self."ipdb"
711 self."ipython"
715 self."ipython"
712 self."pytest"
716 self."pytest"
713 self."py"
717 self."py"
714 self."pytest-cov"
718 self."pytest-cov"
715 self."pytest-sugar"
719 self."pytest-sugar"
716 self."pytest-runner"
720 self."pytest-runner"
717 self."pytest-profiling"
721 self."pytest-profiling"
718 self."pytest-timeout"
722 self."pytest-timeout"
719 self."gprof2dot"
723 self."gprof2dot"
720 self."mock"
724 self."mock"
721 self."webtest"
722 self."cov-core"
725 self."cov-core"
723 self."coverage"
726 self."coverage"
727 self."webtest"
728 self."beautifulsoup4"
724 ];
729 ];
725 src = ./.;
730 src = ./.;
726 meta = {
731 meta = {
727 license = [ { fullName = "GPL V3"; } { fullName = "GNU General Public License v3 or later (GPLv3+)"; } ];
732 license = [ { fullName = "GPL V3"; } { fullName = "GNU General Public License v3 or later (GPLv3+)"; } ];
728 };
733 };
729 };
734 };
730 "scandir" = super.buildPythonPackage {
735 "scandir" = super.buildPythonPackage {
731 name = "scandir-1.9.0";
736 name = "scandir-1.9.0";
732 doCheck = false;
737 doCheck = false;
733 src = fetchurl {
738 src = fetchurl {
734 url = "https://files.pythonhosted.org/packages/16/2a/557af1181e6b4e30254d5a6163b18f5053791ca66e251e77ab08887e8fe3/scandir-1.9.0.tar.gz";
739 url = "https://files.pythonhosted.org/packages/16/2a/557af1181e6b4e30254d5a6163b18f5053791ca66e251e77ab08887e8fe3/scandir-1.9.0.tar.gz";
735 sha256 = "0r3hvf1a9jm1rkqgx40gxkmccknkaiqjavs8lccgq9s8khh5x5s4";
740 sha256 = "0r3hvf1a9jm1rkqgx40gxkmccknkaiqjavs8lccgq9s8khh5x5s4";
736 };
741 };
737 meta = {
742 meta = {
738 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "New BSD License"; } ];
743 license = [ pkgs.lib.licenses.bsdOriginal { fullName = "New BSD License"; } ];
739 };
744 };
740 };
745 };
741 "setproctitle" = super.buildPythonPackage {
746 "setproctitle" = super.buildPythonPackage {
742 name = "setproctitle-1.1.10";
747 name = "setproctitle-1.1.10";
743 doCheck = false;
748 doCheck = false;
744 src = fetchurl {
749 src = fetchurl {
745 url = "https://files.pythonhosted.org/packages/5a/0d/dc0d2234aacba6cf1a729964383e3452c52096dc695581248b548786f2b3/setproctitle-1.1.10.tar.gz";
750 url = "https://files.pythonhosted.org/packages/5a/0d/dc0d2234aacba6cf1a729964383e3452c52096dc695581248b548786f2b3/setproctitle-1.1.10.tar.gz";
746 sha256 = "163kplw9dcrw0lffq1bvli5yws3rngpnvrxrzdw89pbphjjvg0v2";
751 sha256 = "163kplw9dcrw0lffq1bvli5yws3rngpnvrxrzdw89pbphjjvg0v2";
747 };
752 };
748 meta = {
753 meta = {
749 license = [ pkgs.lib.licenses.bsdOriginal ];
754 license = [ pkgs.lib.licenses.bsdOriginal ];
750 };
755 };
751 };
756 };
752 "setuptools" = super.buildPythonPackage {
757 "setuptools" = super.buildPythonPackage {
753 name = "setuptools-40.6.2";
758 name = "setuptools-40.7.3";
754 doCheck = false;
759 doCheck = false;
755 src = fetchurl {
760 src = fetchurl {
756 url = "https://files.pythonhosted.org/packages/b0/d1/8acb42f391cba52e35b131e442e80deffbb8d0676b93261d761b1f0ef8fb/setuptools-40.6.2.zip";
761 url = "https://files.pythonhosted.org/packages/90/86/00ab839a8647e0fee435fe77f70795865ca4534387872a6c23b22fd85ac8/setuptools-40.7.3.zip";
757 sha256 = "0r2c5hapirlzm34h7pl1lgkm6gk7bcrlrdj28qgsvaqg3f74vfw6";
762 sha256 = "0c54kklk7c97g0dhm7ilg9cd4n4s464ps6mspsa5m9hhrcqxsbvh";
758 };
763 };
759 meta = {
764 meta = {
760 license = [ pkgs.lib.licenses.mit ];
765 license = [ pkgs.lib.licenses.mit ];
761 };
766 };
762 };
767 };
763 "simplegeneric" = super.buildPythonPackage {
768 "simplegeneric" = super.buildPythonPackage {
764 name = "simplegeneric-0.8.1";
769 name = "simplegeneric-0.8.1";
765 doCheck = false;
770 doCheck = false;
766 src = fetchurl {
771 src = fetchurl {
767 url = "https://files.pythonhosted.org/packages/3d/57/4d9c9e3ae9a255cd4e1106bb57e24056d3d0709fc01b2e3e345898e49d5b/simplegeneric-0.8.1.zip";
772 url = "https://files.pythonhosted.org/packages/3d/57/4d9c9e3ae9a255cd4e1106bb57e24056d3d0709fc01b2e3e345898e49d5b/simplegeneric-0.8.1.zip";
768 sha256 = "0wwi1c6md4vkbcsfsf8dklf3vr4mcdj4mpxkanwgb6jb1432x5yw";
773 sha256 = "0wwi1c6md4vkbcsfsf8dklf3vr4mcdj4mpxkanwgb6jb1432x5yw";
769 };
774 };
770 meta = {
775 meta = {
771 license = [ pkgs.lib.licenses.zpl21 ];
776 license = [ pkgs.lib.licenses.zpl21 ];
772 };
777 };
773 };
778 };
774 "simplejson" = super.buildPythonPackage {
779 "simplejson" = super.buildPythonPackage {
775 name = "simplejson-3.11.1";
780 name = "simplejson-3.16.0";
776 doCheck = false;
781 doCheck = false;
777 src = fetchurl {
782 src = fetchurl {
778 url = "https://files.pythonhosted.org/packages/08/48/c97b668d6da7d7bebe7ea1817a6f76394b0ec959cb04214ca833c34359df/simplejson-3.11.1.tar.gz";
783 url = "https://files.pythonhosted.org/packages/e3/24/c35fb1c1c315fc0fffe61ea00d3f88e85469004713dab488dee4f35b0aff/simplejson-3.16.0.tar.gz";
779 sha256 = "1rr58dppsq73p0qcd9bsw066cdd3v63sqv7j6sqni8frvm4jv8h1";
784 sha256 = "19cws1syk8jzq2pw43878dv6fjkb0ifvjpx0i9aajix6kc9jkwxi";
780 };
785 };
781 meta = {
786 meta = {
782 license = [ { fullName = "Academic Free License (AFL)"; } pkgs.lib.licenses.mit ];
787 license = [ { fullName = "Academic Free License (AFL)"; } pkgs.lib.licenses.mit ];
783 };
788 };
784 };
789 };
785 "six" = super.buildPythonPackage {
790 "six" = super.buildPythonPackage {
786 name = "six-1.11.0";
791 name = "six-1.11.0";
787 doCheck = false;
792 doCheck = false;
788 src = fetchurl {
793 src = fetchurl {
789 url = "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz";
794 url = "https://files.pythonhosted.org/packages/16/d8/bc6316cf98419719bd59c91742194c111b6f2e85abac88e496adefaf7afe/six-1.11.0.tar.gz";
790 sha256 = "1scqzwc51c875z23phj48gircqjgnn3af8zy2izjwmnlxrxsgs3h";
795 sha256 = "1scqzwc51c875z23phj48gircqjgnn3af8zy2izjwmnlxrxsgs3h";
791 };
796 };
792 meta = {
797 meta = {
793 license = [ pkgs.lib.licenses.mit ];
798 license = [ pkgs.lib.licenses.mit ];
794 };
799 };
795 };
800 };
796 "subprocess32" = super.buildPythonPackage {
801 "subprocess32" = super.buildPythonPackage {
797 name = "subprocess32-3.5.2";
802 name = "subprocess32-3.5.3";
798 doCheck = false;
803 doCheck = false;
799 src = fetchurl {
804 src = fetchurl {
800 url = "https://files.pythonhosted.org/packages/c3/5f/7117737fc7114061837a4f51670d863dd7f7f9c762a6546fa8a0dcfe61c8/subprocess32-3.5.2.tar.gz";
805 url = "https://files.pythonhosted.org/packages/be/2b/beeba583e9877e64db10b52a96915afc0feabf7144dcbf2a0d0ea68bf73d/subprocess32-3.5.3.tar.gz";
801 sha256 = "11v62shwmdys48g7ncs3a8jwwnkcl8d4zcwy6dk73z1zy2f9hazb";
806 sha256 = "1hr5fan8i719hmlmz73hf8rhq74014w07d8ryg7krvvf6692kj3b";
802 };
807 };
803 meta = {
808 meta = {
804 license = [ pkgs.lib.licenses.psfl ];
809 license = [ pkgs.lib.licenses.psfl ];
805 };
810 };
806 };
811 };
807 "subvertpy" = super.buildPythonPackage {
812 "subvertpy" = super.buildPythonPackage {
808 name = "subvertpy-0.10.1";
813 name = "subvertpy-0.10.1";
809 doCheck = false;
814 doCheck = false;
810 src = fetchurl {
815 src = fetchurl {
811 url = "https://files.pythonhosted.org/packages/9d/76/99fa82affce75f5ac0f7dbe513796c3f37311ace0c68e1b063683b4f9b99/subvertpy-0.10.1.tar.gz";
816 url = "https://files.pythonhosted.org/packages/9d/76/99fa82affce75f5ac0f7dbe513796c3f37311ace0c68e1b063683b4f9b99/subvertpy-0.10.1.tar.gz";
812 sha256 = "061ncy9wjz3zyv527avcrdyk0xygyssyy7p1644nhzhwp8zpybij";
817 sha256 = "061ncy9wjz3zyv527avcrdyk0xygyssyy7p1644nhzhwp8zpybij";
813 };
818 };
814 meta = {
819 meta = {
815 license = [ pkgs.lib.licenses.lgpl21Plus pkgs.lib.licenses.gpl2Plus ];
820 license = [ pkgs.lib.licenses.lgpl21Plus pkgs.lib.licenses.gpl2Plus ];
816 };
821 };
817 };
822 };
818 "termcolor" = super.buildPythonPackage {
823 "termcolor" = super.buildPythonPackage {
819 name = "termcolor-1.1.0";
824 name = "termcolor-1.1.0";
820 doCheck = false;
825 doCheck = false;
821 src = fetchurl {
826 src = fetchurl {
822 url = "https://files.pythonhosted.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz";
827 url = "https://files.pythonhosted.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz";
823 sha256 = "0fv1vq14rpqwgazxg4981904lfyp84mnammw7y046491cv76jv8x";
828 sha256 = "0fv1vq14rpqwgazxg4981904lfyp84mnammw7y046491cv76jv8x";
824 };
829 };
825 meta = {
830 meta = {
826 license = [ pkgs.lib.licenses.mit ];
831 license = [ pkgs.lib.licenses.mit ];
827 };
832 };
828 };
833 };
829 "traitlets" = super.buildPythonPackage {
834 "traitlets" = super.buildPythonPackage {
830 name = "traitlets-4.3.2";
835 name = "traitlets-4.3.2";
831 doCheck = false;
836 doCheck = false;
832 propagatedBuildInputs = [
837 propagatedBuildInputs = [
833 self."ipython-genutils"
838 self."ipython-genutils"
834 self."six"
839 self."six"
835 self."decorator"
840 self."decorator"
836 self."enum34"
841 self."enum34"
837 ];
842 ];
838 src = fetchurl {
843 src = fetchurl {
839 url = "https://files.pythonhosted.org/packages/a5/98/7f5ef2fe9e9e071813aaf9cb91d1a732e0a68b6c44a32b38cb8e14c3f069/traitlets-4.3.2.tar.gz";
844 url = "https://files.pythonhosted.org/packages/a5/98/7f5ef2fe9e9e071813aaf9cb91d1a732e0a68b6c44a32b38cb8e14c3f069/traitlets-4.3.2.tar.gz";
840 sha256 = "0dbq7sx26xqz5ixs711k5nc88p8a0nqyz6162pwks5dpcz9d4jww";
845 sha256 = "0dbq7sx26xqz5ixs711k5nc88p8a0nqyz6162pwks5dpcz9d4jww";
841 };
846 };
842 meta = {
847 meta = {
843 license = [ pkgs.lib.licenses.bsdOriginal ];
848 license = [ pkgs.lib.licenses.bsdOriginal ];
844 };
849 };
845 };
850 };
846 "translationstring" = super.buildPythonPackage {
851 "translationstring" = super.buildPythonPackage {
847 name = "translationstring-1.3";
852 name = "translationstring-1.3";
848 doCheck = false;
853 doCheck = false;
849 src = fetchurl {
854 src = fetchurl {
850 url = "https://files.pythonhosted.org/packages/5e/eb/bee578cc150b44c653b63f5ebe258b5d0d812ddac12497e5f80fcad5d0b4/translationstring-1.3.tar.gz";
855 url = "https://files.pythonhosted.org/packages/5e/eb/bee578cc150b44c653b63f5ebe258b5d0d812ddac12497e5f80fcad5d0b4/translationstring-1.3.tar.gz";
851 sha256 = "0bdpcnd9pv0131dl08h4zbcwmgc45lyvq3pa224xwan5b3x4rr2f";
856 sha256 = "0bdpcnd9pv0131dl08h4zbcwmgc45lyvq3pa224xwan5b3x4rr2f";
852 };
857 };
853 meta = {
858 meta = {
854 license = [ { fullName = "BSD-like (http://repoze.org/license.html)"; } ];
859 license = [ { fullName = "BSD-like (http://repoze.org/license.html)"; } ];
855 };
860 };
856 };
861 };
857 "venusian" = super.buildPythonPackage {
862 "venusian" = super.buildPythonPackage {
858 name = "venusian-1.1.0";
863 name = "venusian-1.2.0";
859 doCheck = false;
864 doCheck = false;
860 src = fetchurl {
865 src = fetchurl {
861 url = "https://files.pythonhosted.org/packages/38/24/b4b470ab9e0a2e2e9b9030c7735828c8934b4c6b45befd1bb713ec2aeb2d/venusian-1.1.0.tar.gz";
866 url = "https://files.pythonhosted.org/packages/7e/6f/40a9d43ac77cb51cb62be5b5662d170f43f8037bdc4eab56336c4ca92bb7/venusian-1.2.0.tar.gz";
862 sha256 = "0zapz131686qm0gazwy8bh11vr57pr89jbwbl50s528sqy9f80lr";
867 sha256 = "0ghyx66g8ikx9nx1mnwqvdcqm11i1vlq0hnvwl50s48bp22q5v34";
863 };
868 };
864 meta = {
869 meta = {
865 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
870 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
866 };
871 };
867 };
872 };
868 "waitress" = super.buildPythonPackage {
873 "waitress" = super.buildPythonPackage {
869 name = "waitress-1.1.0";
874 name = "waitress-1.1.0";
870 doCheck = false;
875 doCheck = false;
871 src = fetchurl {
876 src = fetchurl {
872 url = "https://files.pythonhosted.org/packages/3c/68/1c10dd5c556872ceebe88483b0436140048d39de83a84a06a8baa8136f4f/waitress-1.1.0.tar.gz";
877 url = "https://files.pythonhosted.org/packages/3c/68/1c10dd5c556872ceebe88483b0436140048d39de83a84a06a8baa8136f4f/waitress-1.1.0.tar.gz";
873 sha256 = "1a85gyji0kajc3p0s1pwwfm06w4wfxjkvvl4rnrz3h164kbd6g6k";
878 sha256 = "1a85gyji0kajc3p0s1pwwfm06w4wfxjkvvl4rnrz3h164kbd6g6k";
874 };
879 };
875 meta = {
880 meta = {
876 license = [ pkgs.lib.licenses.zpl21 ];
881 license = [ pkgs.lib.licenses.zpl21 ];
877 };
882 };
878 };
883 };
879 "wcwidth" = super.buildPythonPackage {
884 "wcwidth" = super.buildPythonPackage {
880 name = "wcwidth-0.1.7";
885 name = "wcwidth-0.1.7";
881 doCheck = false;
886 doCheck = false;
882 src = fetchurl {
887 src = fetchurl {
883 url = "https://files.pythonhosted.org/packages/55/11/e4a2bb08bb450fdbd42cc709dd40de4ed2c472cf0ccb9e64af22279c5495/wcwidth-0.1.7.tar.gz";
888 url = "https://files.pythonhosted.org/packages/55/11/e4a2bb08bb450fdbd42cc709dd40de4ed2c472cf0ccb9e64af22279c5495/wcwidth-0.1.7.tar.gz";
884 sha256 = "0pn6dflzm609m4r3i8ik5ni9ijjbb5fa3vg1n7hn6vkd49r77wrx";
889 sha256 = "0pn6dflzm609m4r3i8ik5ni9ijjbb5fa3vg1n7hn6vkd49r77wrx";
885 };
890 };
886 meta = {
891 meta = {
887 license = [ pkgs.lib.licenses.mit ];
892 license = [ pkgs.lib.licenses.mit ];
888 };
893 };
889 };
894 };
890 "webob" = super.buildPythonPackage {
895 "webob" = super.buildPythonPackage {
891 name = "webob-1.7.4";
896 name = "webob-1.8.4";
892 doCheck = false;
897 doCheck = false;
893 src = fetchurl {
898 src = fetchurl {
894 url = "https://files.pythonhosted.org/packages/75/34/731e23f52371852dfe7490a61644826ba7fe70fd52a377aaca0f4956ba7f/WebOb-1.7.4.tar.gz";
899 url = "https://files.pythonhosted.org/packages/e4/6c/99e322c3d4cc11d9060a67a9bf2f7c9c581f40988c11fffe89bb8c36bc5e/WebOb-1.8.4.tar.gz";
895 sha256 = "1na01ljg04z40il7vcrn8g29vaw7nvg1xvhk64cr4jys5wcay44d";
900 sha256 = "16cfg5y4n6sihz59vsmns2yqbfm0gfsn3l5xgz2g0pdhilaib0x4";
896 };
901 };
897 meta = {
902 meta = {
898 license = [ pkgs.lib.licenses.mit ];
903 license = [ pkgs.lib.licenses.mit ];
899 };
904 };
900 };
905 };
901 "webtest" = super.buildPythonPackage {
906 "webtest" = super.buildPythonPackage {
902 name = "webtest-2.0.29";
907 name = "webtest-2.0.32";
903 doCheck = false;
908 doCheck = false;
904 propagatedBuildInputs = [
909 propagatedBuildInputs = [
905 self."six"
910 self."six"
906 self."webob"
911 self."webob"
907 self."waitress"
912 self."waitress"
908 self."beautifulsoup4"
913 self."beautifulsoup4"
909 ];
914 ];
910 src = fetchurl {
915 src = fetchurl {
911 url = "https://files.pythonhosted.org/packages/94/de/8f94738be649997da99c47b104aa3c3984ecec51a1d8153ed09638253d56/WebTest-2.0.29.tar.gz";
916 url = "https://files.pythonhosted.org/packages/27/9f/9e74449d272ffbef4fb3012e6dbc53c0b24822d545e7a33a342f80131e59/WebTest-2.0.32.tar.gz";
912 sha256 = "0bcj1ica5lnmj5zbvk46x28kgphcsgh7sfnwjmn0cr94mhawrg6v";
917 sha256 = "0qp0nnbazzm4ibjiyqfcn6f230svk09i4g58zg2i9x1ga06h48a2";
913 };
918 };
914 meta = {
919 meta = {
915 license = [ pkgs.lib.licenses.mit ];
920 license = [ pkgs.lib.licenses.mit ];
916 };
921 };
917 };
922 };
918 "zope.deprecation" = super.buildPythonPackage {
923 "zope.deprecation" = super.buildPythonPackage {
919 name = "zope.deprecation-4.3.0";
924 name = "zope.deprecation-4.3.0";
920 doCheck = false;
925 doCheck = false;
921 propagatedBuildInputs = [
926 propagatedBuildInputs = [
922 self."setuptools"
927 self."setuptools"
923 ];
928 ];
924 src = fetchurl {
929 src = fetchurl {
925 url = "https://files.pythonhosted.org/packages/a1/18/2dc5e6bfe64fdc3b79411b67464c55bb0b43b127051a20f7f492ab767758/zope.deprecation-4.3.0.tar.gz";
930 url = "https://files.pythonhosted.org/packages/a1/18/2dc5e6bfe64fdc3b79411b67464c55bb0b43b127051a20f7f492ab767758/zope.deprecation-4.3.0.tar.gz";
926 sha256 = "095jas41wbxgmw95kwdxqhbc3bgihw2hzj9b3qpdg85apcsf2lkx";
931 sha256 = "095jas41wbxgmw95kwdxqhbc3bgihw2hzj9b3qpdg85apcsf2lkx";
927 };
932 };
928 meta = {
933 meta = {
929 license = [ pkgs.lib.licenses.zpl21 ];
934 license = [ pkgs.lib.licenses.zpl21 ];
930 };
935 };
931 };
936 };
932 "zope.interface" = super.buildPythonPackage {
937 "zope.interface" = super.buildPythonPackage {
933 name = "zope.interface-4.5.0";
938 name = "zope.interface-4.5.0";
934 doCheck = false;
939 doCheck = false;
935 propagatedBuildInputs = [
940 propagatedBuildInputs = [
936 self."setuptools"
941 self."setuptools"
937 ];
942 ];
938 src = fetchurl {
943 src = fetchurl {
939 url = "https://files.pythonhosted.org/packages/ac/8a/657532df378c2cd2a1fe6b12be3b4097521570769d4852ec02c24bd3594e/zope.interface-4.5.0.tar.gz";
944 url = "https://files.pythonhosted.org/packages/ac/8a/657532df378c2cd2a1fe6b12be3b4097521570769d4852ec02c24bd3594e/zope.interface-4.5.0.tar.gz";
940 sha256 = "0k67m60ij06wkg82n15qgyn96waf4pmrkhv0njpkfzpmv5q89hsp";
945 sha256 = "0k67m60ij06wkg82n15qgyn96waf4pmrkhv0njpkfzpmv5q89hsp";
941 };
946 };
942 meta = {
947 meta = {
943 license = [ pkgs.lib.licenses.zpl21 ];
948 license = [ pkgs.lib.licenses.zpl21 ];
944 };
949 };
945 };
950 };
946
951
947 ### Test requirements
952 ### Test requirements
948
953
949
954
950 }
955 }
@@ -1,14 +1,22 b''
1 # This file defines how to "build" for packaging.
1 # This file defines how to "build" for packaging.
2
2
3 { doCheck ? true
3 { pkgs ? import <nixpkgs> {}
4 , system ? builtins.currentSystem
5 , doCheck ? false
4 }:
6 }:
5
7
6 let
8 let
7 vcsserver = import ./default.nix {
9 vcsserver = import ./default.nix {
8 inherit
10 inherit
9 doCheck;
11 doCheck
12 system;
13
14 # disable checkPhase for build
15 checkPhase = ''
16 '';
17
10 };
18 };
11
19
12 in {
20 in {
13 build = vcsserver;
21 build = vcsserver;
14 }
22 }
@@ -1,48 +1,49 b''
1 ## dependencies
1 ## dependencies
2
2
3 # our custom configobj
3 # our custom configobj
4 https://code.rhodecode.com/upstream/configobj/archive/a11ff0a0bd4fbda9e3a91267e720f88329efb4a6.tar.gz?md5=9916c524ea11a6c418217af6b28d4b3c#egg=configobj==5.0.6
4 https://code.rhodecode.com/upstream/configobj/archive/a11ff0a0bd4fbda9e3a91267e720f88329efb4a6.tar.gz?md5=9916c524ea11a6c418217af6b28d4b3c#egg=configobj==5.0.6
5 atomicwrites==1.2.1
5 atomicwrites==1.2.1
6 attrs==18.2.0
6 attrs==18.2.0
7 dogpile.cache==0.6.7
7 dogpile.cache==0.7.1
8 dogpile.core==0.4.1
8 dogpile.core==0.4.1
9 decorator==4.1.2
9 decorator==4.1.2
10 dulwich==0.13.0
10 dulwich==0.13.0
11 hgsubversion==1.9.3
11 hgsubversion==1.9.3
12 hg-evolve==8.0.1
12 hg-evolve==8.0.1
13 mako==1.0.7
13 mako==1.0.7
14 markupsafe==1.0.0
14 markupsafe==1.1.0
15 mercurial==4.6.2
15 mercurial==4.6.2
16 msgpack-python==0.5.6
16 msgpack-python==0.5.6
17
17
18 pastedeploy==1.5.2
18 pastedeploy==2.0.1
19 psutil==5.4.7
19 psutil==5.4.8
20 pyramid==1.9.2
20 pyramid==1.10.1
21 pyramid-mako==1.0.2
21 pyramid-mako==1.0.2
22
22
23 pygments==2.3.0
23 pygments==2.3.1
24 pathlib2==2.3.2
24 pathlib2==2.3.3
25 repoze.lru==0.7
25 repoze.lru==0.7
26 simplejson==3.11.1
26 simplejson==3.16.0
27 subprocess32==3.5.2
27 subprocess32==3.5.3
28 subvertpy==0.10.1
28 subvertpy==0.10.1
29
29
30 six==1.11.0
30 six==1.11.0
31 translationstring==1.3
31 translationstring==1.3
32 webob==1.7.4
32 webob==1.8.4
33 zope.deprecation==4.3.0
33 zope.deprecation==4.3.0
34 zope.interface==4.5.0
34 zope.interface==4.5.0
35 venusian==1.2.0
35
36
36 ## http servers
37 ## http servers
37 gevent==1.3.7
38 gevent==1.4.0
38 greenlet==0.4.15
39 greenlet==0.4.15
39 gunicorn==19.9.0
40 gunicorn==19.9.0
40 waitress==1.1.0
41 waitress==1.1.0
41 setproctitle==1.1.10
42 setproctitle==1.1.10
42
43
43 ## debug
44 ## debug
44 ipdb==0.11.0
45 ipdb==0.11.0
45 ipython==5.1.0
46 ipython==5.1.0
46
47
47 ## test related requirements
48 ## test related requirements
48 -r requirements_test.txt
49 -r requirements_test.txt
@@ -1,14 +1,16 b''
1 # test related requirements
1 # test related requirements
2 pytest==3.8.2
2 pytest==3.8.2
3 py==1.6.0
3 py==1.6.0
4 pytest-cov==2.6.0
4 pytest-cov==2.6.0
5 pytest-sugar==0.9.1
5 pytest-sugar==0.9.1
6 pytest-runner==4.2.0
6 pytest-runner==4.2.0
7 pytest-profiling==1.3.0
7 pytest-profiling==1.3.0
8 pytest-timeout==1.3.2
8 pytest-timeout==1.3.2
9 gprof2dot==2017.9.19
9 gprof2dot==2017.9.19
10
10
11 mock==1.0.1
11 mock==1.0.1
12 webtest==2.0.29
13 cov-core==1.15.0
12 cov-core==1.15.0
14 coverage==4.5.1
13 coverage==4.5.1
14
15 webtest==2.0.32
16 beautifulsoup4==4.6.3
@@ -1,136 +1,136 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # Copyright (C) 2014-2017 RodeCode GmbH
3 # Copyright (C) 2014-2019 RodeCode GmbH
4 #
4 #
5 # This program is free software; you can redistribute it and/or modify
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
8 # (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
18
19 # Import early to make sure things are patched up properly
19 # Import early to make sure things are patched up properly
20 from setuptools import setup, find_packages
20 from setuptools import setup, find_packages
21
21
22 import os
22 import os
23 import sys
23 import sys
24 import pkgutil
24 import pkgutil
25 import platform
25 import platform
26 import codecs
26 import codecs
27
27
28 try: # for pip >= 10
28 try: # for pip >= 10
29 from pip._internal.req import parse_requirements
29 from pip._internal.req import parse_requirements
30 except ImportError: # for pip <= 9.0.3
30 except ImportError: # for pip <= 9.0.3
31 from pip.req import parse_requirements
31 from pip.req import parse_requirements
32
32
33 try: # for pip >= 10
33 try: # for pip >= 10
34 from pip._internal.download import PipSession
34 from pip._internal.download import PipSession
35 except ImportError: # for pip <= 9.0.3
35 except ImportError: # for pip <= 9.0.3
36 from pip.download import PipSession
36 from pip.download import PipSession
37
37
38
38
39
39
40 if sys.version_info < (2, 7):
40 if sys.version_info < (2, 7):
41 raise Exception('VCSServer requires Python 2.7 or later')
41 raise Exception('VCSServer requires Python 2.7 or later')
42
42
43 here = os.path.abspath(os.path.dirname(__file__))
43 here = os.path.abspath(os.path.dirname(__file__))
44
44
45 # defines current platform
45 # defines current platform
46 __platform__ = platform.system()
46 __platform__ = platform.system()
47 __license__ = 'GPL V3'
47 __license__ = 'GPL V3'
48 __author__ = 'RhodeCode GmbH'
48 __author__ = 'RhodeCode GmbH'
49 __url__ = 'https://code.rhodecode.com'
49 __url__ = 'https://code.rhodecode.com'
50 is_windows = __platform__ in ('Windows',)
50 is_windows = __platform__ in ('Windows',)
51
51
52
52
53 def _get_requirements(req_filename, exclude=None, extras=None):
53 def _get_requirements(req_filename, exclude=None, extras=None):
54 extras = extras or []
54 extras = extras or []
55 exclude = exclude or []
55 exclude = exclude or []
56
56
57 try:
57 try:
58 parsed = parse_requirements(
58 parsed = parse_requirements(
59 os.path.join(here, req_filename), session=PipSession())
59 os.path.join(here, req_filename), session=PipSession())
60 except TypeError:
60 except TypeError:
61 # try pip < 6.0.0, that doesn't support session
61 # try pip < 6.0.0, that doesn't support session
62 parsed = parse_requirements(os.path.join(here, req_filename))
62 parsed = parse_requirements(os.path.join(here, req_filename))
63
63
64 requirements = []
64 requirements = []
65 for ir in parsed:
65 for ir in parsed:
66 if ir.req and ir.name not in exclude:
66 if ir.req and ir.name not in exclude:
67 requirements.append(str(ir.req))
67 requirements.append(str(ir.req))
68 return requirements + extras
68 return requirements + extras
69
69
70
70
71 # requirements extract
71 # requirements extract
72 setup_requirements = ['pytest-runner']
72 setup_requirements = ['pytest-runner']
73 install_requirements = _get_requirements(
73 install_requirements = _get_requirements(
74 'requirements.txt', exclude=['setuptools'])
74 'requirements.txt', exclude=['setuptools'])
75 test_requirements = _get_requirements(
75 test_requirements = _get_requirements(
76 'requirements_test.txt', extras=['configobj'])
76 'requirements_test.txt', extras=['configobj'])
77
77
78
78
79 def get_version():
79 def get_version():
80 version = pkgutil.get_data('vcsserver', 'VERSION')
80 version = pkgutil.get_data('vcsserver', 'VERSION')
81 return version.strip()
81 return version.strip()
82
82
83
83
84 # additional files that goes into package itself
84 # additional files that goes into package itself
85 package_data = {
85 package_data = {
86 '': ['*.txt', '*.rst'],
86 '': ['*.txt', '*.rst'],
87 'configs': ['*.ini'],
87 'configs': ['*.ini'],
88 'vcsserver': ['VERSION'],
88 'vcsserver': ['VERSION'],
89 }
89 }
90
90
91 description = 'Version Control System Server'
91 description = 'Version Control System Server'
92 keywords = ' '.join([
92 keywords = ' '.join([
93 'CLI', 'RhodeCode', 'RhodeCode Enterprise', 'RhodeCode Tools'])
93 'CLI', 'RhodeCode', 'RhodeCode Enterprise', 'RhodeCode Tools'])
94
94
95 # README/DESCRIPTION generation
95 # README/DESCRIPTION generation
96 readme_file = 'README.rst'
96 readme_file = 'README.rst'
97 changelog_file = 'CHANGES.rst'
97 changelog_file = 'CHANGES.rst'
98 try:
98 try:
99 long_description = codecs.open(readme_file).read() + '\n\n' + \
99 long_description = codecs.open(readme_file).read() + '\n\n' + \
100 codecs.open(changelog_file).read()
100 codecs.open(changelog_file).read()
101 except IOError as err:
101 except IOError as err:
102 sys.stderr.write(
102 sys.stderr.write(
103 "[WARNING] Cannot find file specified as long_description (%s)\n "
103 "[WARNING] Cannot find file specified as long_description (%s)\n "
104 "or changelog (%s) skipping that file" % (readme_file, changelog_file))
104 "or changelog (%s) skipping that file" % (readme_file, changelog_file))
105 long_description = description
105 long_description = description
106
106
107
107
108 setup(
108 setup(
109 name='rhodecode-vcsserver',
109 name='rhodecode-vcsserver',
110 version=get_version(),
110 version=get_version(),
111 description=description,
111 description=description,
112 long_description=long_description,
112 long_description=long_description,
113 keywords=keywords,
113 keywords=keywords,
114 license=__license__,
114 license=__license__,
115 author=__author__,
115 author=__author__,
116 author_email='admin@rhodecode.com',
116 author_email='admin@rhodecode.com',
117 url=__url__,
117 url=__url__,
118 setup_requires=setup_requirements,
118 setup_requires=setup_requirements,
119 install_requires=install_requirements,
119 install_requires=install_requirements,
120 tests_require=test_requirements,
120 tests_require=test_requirements,
121 zip_safe=False,
121 zip_safe=False,
122 packages=find_packages(exclude=["docs", "tests*"]),
122 packages=find_packages(exclude=["docs", "tests*"]),
123 package_data=package_data,
123 package_data=package_data,
124 include_package_data=True,
124 include_package_data=True,
125 classifiers=[
125 classifiers=[
126 'Development Status :: 6 - Mature',
126 'Development Status :: 6 - Mature',
127 'Intended Audience :: Developers',
127 'Intended Audience :: Developers',
128 'Operating System :: OS Independent',
128 'Operating System :: OS Independent',
129 'Topic :: Software Development :: Version Control',
129 'Topic :: Software Development :: Version Control',
130 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
130 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
131 'Programming Language :: Python :: 2.7',
131 'Programming Language :: Python :: 2.7',
132 ],
132 ],
133 entry_points={
133 entry_points={
134 'paste.app_factory': ['main=vcsserver.http_main:main']
134 'paste.app_factory': ['main=vcsserver.http_main:main']
135 },
135 },
136 )
136 )
@@ -1,1 +1,1 b''
1 4.15.2 No newline at end of file
1 4.16.0 No newline at end of file
@@ -1,28 +1,28 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import pkgutil
18 import pkgutil
19
19
20
20
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip()
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip()
22
22
23 # link to config for pyramid
23 # link to config for pyramid
24 CONFIG = {}
24 CONFIG = {}
25
25
26 # Populated with the settings dictionary from application init in
26 # Populated with the settings dictionary from application init in
27 #
27 #
28 PYRAMID_SETTINGS = {}
28 PYRAMID_SETTINGS = {}
@@ -1,91 +1,94 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import sys
18 import sys
19 import traceback
19 import traceback
20 import logging
20 import logging
21 import urlparse
21 import urlparse
22
22
23 from vcsserver.lib.rc_cache import region_meta
23 from vcsserver.lib.rc_cache import region_meta
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 class RepoFactory(object):
27 class RepoFactory(object):
28 """
28 """
29 Utility to create instances of repository
29 Utility to create instances of repository
30
30
31 It provides internal caching of the `repo` object based on
31 It provides internal caching of the `repo` object based on
32 the :term:`call context`.
32 the :term:`call context`.
33 """
33 """
34 repo_type = None
34 repo_type = None
35
35
36 def __init__(self):
36 def __init__(self):
37 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
37 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
38
38
39 def _create_config(self, path, config):
39 def _create_config(self, path, config):
40 config = {}
40 config = {}
41 return config
41 return config
42
42
43 def _create_repo(self, wire, create):
43 def _create_repo(self, wire, create):
44 raise NotImplementedError()
44 raise NotImplementedError()
45
45
46 def repo(self, wire, create=False):
46 def repo(self, wire, create=False):
47 """
47 """
48 Get a repository instance for the given path.
48 Get a repository instance for the given path.
49
49
50 Uses internally the low level beaker API since the decorators introduce
50 Uses internally the low level beaker API since the decorators introduce
51 significant overhead.
51 significant overhead.
52 """
52 """
53 region = self._cache_region
53 region = self._cache_region
54 context = wire.get('context', None)
54 context = wire.get('context', None)
55 repo_path = wire.get('path', '')
55 repo_path = wire.get('path', '')
56 context_uid = '{}'.format(context)
56 context_uid = '{}'.format(context)
57 cache = wire.get('cache', True)
57 cache = wire.get('cache', True)
58 cache_on = context and cache
58 cache_on = context and cache
59
59
60 @region.conditional_cache_on_arguments(condition=cache_on)
60 @region.conditional_cache_on_arguments(condition=cache_on)
61 def create_new_repo(_repo_type, _repo_path, _context_uid):
61 def create_new_repo(_repo_type, _repo_path, _context_uid):
62 return self._create_repo(wire, create)
62 return self._create_repo(wire, create)
63
63
64 repo = create_new_repo(self.repo_type, repo_path, context_uid)
64 repo = create_new_repo(self.repo_type, repo_path, context_uid)
65 return repo
65 return repo
66
66
67
67
68 def obfuscate_qs(query_string):
68 def obfuscate_qs(query_string):
69 if query_string is None:
69 if query_string is None:
70 return None
70 return None
71
71
72 parsed = []
72 parsed = []
73 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
73 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
74 if k in ['auth_token', 'api_key']:
74 if k in ['auth_token', 'api_key']:
75 v = "*****"
75 v = "*****"
76 parsed.append((k, v))
76 parsed.append((k, v))
77
77
78 return '&'.join('{}{}'.format(
78 return '&'.join('{}{}'.format(
79 k, '={}'.format(v) if v else '') for k, v in parsed)
79 k, '={}'.format(v) if v else '') for k, v in parsed)
80
80
81
81
82 def raise_from_original(new_type):
82 def raise_from_original(new_type):
83 """
83 """
84 Raise a new exception type with original args and traceback.
84 Raise a new exception type with original args and traceback.
85 """
85 """
86 exc_type, exc_value, exc_traceback = sys.exc_info()
86 exc_type, exc_value, exc_traceback = sys.exc_info()
87 new_exc = new_type(*exc_value.args)
88 # store the original traceback into the new exc
89 new_exc._org_exc_tb = traceback.format_exc(exc_traceback)
87
90
88 try:
91 try:
89 raise new_type(*exc_value.args), None, exc_traceback
92 raise new_exc, None, exc_traceback
90 finally:
93 finally:
91 del exc_traceback
94 del exc_traceback
@@ -1,116 +1,117 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Special exception handling over the wire.
19 Special exception handling over the wire.
20
20
21 Since we cannot assume that our client is able to import our exception classes,
21 Since we cannot assume that our client is able to import our exception classes,
22 this module provides a "wrapping" mechanism to raise plain exceptions
22 this module provides a "wrapping" mechanism to raise plain exceptions
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 different error conditions.
24 different error conditions.
25 """
25 """
26
26
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28
28
29
29
30 def _make_exception(kind, org_exc, *args):
30 def _make_exception(kind, org_exc, *args):
31 """
31 """
32 Prepares a base `Exception` instance to be sent over the wire.
32 Prepares a base `Exception` instance to be sent over the wire.
33
33
34 To give our caller a hint what this is about, it will attach an attribute
34 To give our caller a hint what this is about, it will attach an attribute
35 `_vcs_kind` to the exception.
35 `_vcs_kind` to the exception.
36 """
36 """
37 exc = Exception(*args)
37 exc = Exception(*args)
38 exc._vcs_kind = kind
38 exc._vcs_kind = kind
39 exc._org_exc = org_exc
39 exc._org_exc = org_exc
40 exc._org_exc_tb = ''
40 return exc
41 return exc
41
42
42
43
43 def AbortException(org_exc=None):
44 def AbortException(org_exc=None):
44 def _make_exception_wrapper(*args):
45 def _make_exception_wrapper(*args):
45 return _make_exception('abort', org_exc, *args)
46 return _make_exception('abort', org_exc, *args)
46 return _make_exception_wrapper
47 return _make_exception_wrapper
47
48
48
49
49 def ArchiveException(org_exc=None):
50 def ArchiveException(org_exc=None):
50 def _make_exception_wrapper(*args):
51 def _make_exception_wrapper(*args):
51 return _make_exception('archive', org_exc, *args)
52 return _make_exception('archive', org_exc, *args)
52 return _make_exception_wrapper
53 return _make_exception_wrapper
53
54
54
55
55 def LookupException(org_exc=None):
56 def LookupException(org_exc=None):
56 def _make_exception_wrapper(*args):
57 def _make_exception_wrapper(*args):
57 return _make_exception('lookup', org_exc, *args)
58 return _make_exception('lookup', org_exc, *args)
58 return _make_exception_wrapper
59 return _make_exception_wrapper
59
60
60
61
61 def VcsException(org_exc=None):
62 def VcsException(org_exc=None):
62 def _make_exception_wrapper(*args):
63 def _make_exception_wrapper(*args):
63 return _make_exception('error', org_exc, *args)
64 return _make_exception('error', org_exc, *args)
64 return _make_exception_wrapper
65 return _make_exception_wrapper
65
66
66
67
67 def RepositoryLockedException(org_exc=None):
68 def RepositoryLockedException(org_exc=None):
68 def _make_exception_wrapper(*args):
69 def _make_exception_wrapper(*args):
69 return _make_exception('repo_locked', org_exc, *args)
70 return _make_exception('repo_locked', org_exc, *args)
70 return _make_exception_wrapper
71 return _make_exception_wrapper
71
72
72
73
73 def RepositoryBranchProtectedException(org_exc=None):
74 def RepositoryBranchProtectedException(org_exc=None):
74 def _make_exception_wrapper(*args):
75 def _make_exception_wrapper(*args):
75 return _make_exception('repo_branch_protected', org_exc, *args)
76 return _make_exception('repo_branch_protected', org_exc, *args)
76 return _make_exception_wrapper
77 return _make_exception_wrapper
77
78
78
79
79 def RequirementException(org_exc=None):
80 def RequirementException(org_exc=None):
80 def _make_exception_wrapper(*args):
81 def _make_exception_wrapper(*args):
81 return _make_exception('requirement', org_exc, *args)
82 return _make_exception('requirement', org_exc, *args)
82 return _make_exception_wrapper
83 return _make_exception_wrapper
83
84
84
85
85 def UnhandledException(org_exc=None):
86 def UnhandledException(org_exc=None):
86 def _make_exception_wrapper(*args):
87 def _make_exception_wrapper(*args):
87 return _make_exception('unhandled', org_exc, *args)
88 return _make_exception('unhandled', org_exc, *args)
88 return _make_exception_wrapper
89 return _make_exception_wrapper
89
90
90
91
91 def URLError(org_exc=None):
92 def URLError(org_exc=None):
92 def _make_exception_wrapper(*args):
93 def _make_exception_wrapper(*args):
93 return _make_exception('url_error', org_exc, *args)
94 return _make_exception('url_error', org_exc, *args)
94 return _make_exception_wrapper
95 return _make_exception_wrapper
95
96
96
97
97 def SubrepoMergeException(org_exc=None):
98 def SubrepoMergeException(org_exc=None):
98 def _make_exception_wrapper(*args):
99 def _make_exception_wrapper(*args):
99 return _make_exception('subrepo_merge_error', org_exc, *args)
100 return _make_exception('subrepo_merge_error', org_exc, *args)
100 return _make_exception_wrapper
101 return _make_exception_wrapper
101
102
102
103
103 class HTTPRepoLocked(HTTPLocked):
104 class HTTPRepoLocked(HTTPLocked):
104 """
105 """
105 Subclass of HTTPLocked response that allows to set the title and status
106 Subclass of HTTPLocked response that allows to set the title and status
106 code via constructor arguments.
107 code via constructor arguments.
107 """
108 """
108 def __init__(self, title, status_code=None, **kwargs):
109 def __init__(self, title, status_code=None, **kwargs):
109 self.code = status_code or HTTPLocked.code
110 self.code = status_code or HTTPLocked.code
110 self.title = title
111 self.title = title
111 super(HTTPRepoLocked, self).__init__(**kwargs)
112 super(HTTPRepoLocked, self).__init__(**kwargs)
112
113
113
114
114 class HTTPRepoBranchProtected(HTTPForbidden):
115 class HTTPRepoBranchProtected(HTTPForbidden):
115 def __init__(self, *args, **kwargs):
116 def __init__(self, *args, **kwargs):
116 super(HTTPForbidden, self).__init__(*args, **kwargs)
117 super(HTTPForbidden, self).__init__(*args, **kwargs)
@@ -1,719 +1,742 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 import collections
17 import collections
18 import logging
18 import logging
19 import os
19 import os
20 import posixpath as vcspath
20 import posixpath as vcspath
21 import re
21 import re
22 import stat
22 import stat
23 import traceback
23 import traceback
24 import urllib
24 import urllib
25 import urllib2
25 import urllib2
26 from functools import wraps
26 from functools import wraps
27
27
28 import more_itertools
28 from dulwich import index, objects
29 from dulwich import index, objects
29 from dulwich.client import HttpGitClient, LocalGitClient
30 from dulwich.client import HttpGitClient, LocalGitClient
30 from dulwich.errors import (
31 from dulwich.errors import (
31 NotGitRepository, ChecksumMismatch, WrongObjectException,
32 NotGitRepository, ChecksumMismatch, WrongObjectException,
32 MissingCommitError, ObjectMissing, HangupException,
33 MissingCommitError, ObjectMissing, HangupException,
33 UnexpectedCommandError)
34 UnexpectedCommandError)
34 from dulwich.repo import Repo as DulwichRepo, Tag
35 from dulwich.repo import Repo as DulwichRepo, Tag
35 from dulwich.server import update_server_info
36 from dulwich.server import update_server_info
36
37
37 from vcsserver import exceptions, settings, subprocessio
38 from vcsserver import exceptions, settings, subprocessio
38 from vcsserver.utils import safe_str
39 from vcsserver.utils import safe_str
39 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
40 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
40 from vcsserver.hgcompat import (
41 from vcsserver.hgcompat import (
41 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
42 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
42 from vcsserver.git_lfs.lib import LFSOidStore
43 from vcsserver.git_lfs.lib import LFSOidStore
43
44
44 DIR_STAT = stat.S_IFDIR
45 DIR_STAT = stat.S_IFDIR
45 FILE_MODE = stat.S_IFMT
46 FILE_MODE = stat.S_IFMT
46 GIT_LINK = objects.S_IFGITLINK
47 GIT_LINK = objects.S_IFGITLINK
47
48
48 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
49
50
50
51
51 def reraise_safe_exceptions(func):
52 def reraise_safe_exceptions(func):
52 """Converts Dulwich exceptions to something neutral."""
53 """Converts Dulwich exceptions to something neutral."""
53 @wraps(func)
54 @wraps(func)
54 def wrapper(*args, **kwargs):
55 def wrapper(*args, **kwargs):
55 try:
56 try:
56 return func(*args, **kwargs)
57 return func(*args, **kwargs)
57 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
58 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
58 ObjectMissing) as e:
59 ObjectMissing) as e:
59 exc = exceptions.LookupException(e)
60 exc = exceptions.LookupException(e)
60 raise exc(e)
61 raise exc(e)
61 except (HangupException, UnexpectedCommandError) as e:
62 except (HangupException, UnexpectedCommandError) as e:
62 exc = exceptions.VcsException(e)
63 exc = exceptions.VcsException(e)
63 raise exc(e)
64 raise exc(e)
64 except Exception as e:
65 except Exception as e:
65 # NOTE(marcink): becuase of how dulwich handles some exceptions
66 # NOTE(marcink): becuase of how dulwich handles some exceptions
66 # (KeyError on empty repos), we cannot track this and catch all
67 # (KeyError on empty repos), we cannot track this and catch all
67 # exceptions, it's an exceptions from other handlers
68 # exceptions, it's an exceptions from other handlers
68 #if not hasattr(e, '_vcs_kind'):
69 #if not hasattr(e, '_vcs_kind'):
69 #log.exception("Unhandled exception in git remote call")
70 #log.exception("Unhandled exception in git remote call")
70 #raise_from_original(exceptions.UnhandledException)
71 #raise_from_original(exceptions.UnhandledException)
71 raise
72 raise
72 return wrapper
73 return wrapper
73
74
74
75
75 class Repo(DulwichRepo):
76 class Repo(DulwichRepo):
76 """
77 """
77 A wrapper for dulwich Repo class.
78 A wrapper for dulwich Repo class.
78
79
79 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
80 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
80 "Too many open files" error. We need to close all opened file descriptors
81 "Too many open files" error. We need to close all opened file descriptors
81 once the repo object is destroyed.
82 once the repo object is destroyed.
82
83
83 TODO: mikhail: please check if we need this wrapper after updating dulwich
84 TODO: mikhail: please check if we need this wrapper after updating dulwich
84 to 0.12.0 +
85 to 0.12.0 +
85 """
86 """
86 def __del__(self):
87 def __del__(self):
87 if hasattr(self, 'object_store'):
88 if hasattr(self, 'object_store'):
88 self.close()
89 self.close()
89
90
90
91
91 class GitFactory(RepoFactory):
92 class GitFactory(RepoFactory):
92 repo_type = 'git'
93 repo_type = 'git'
93
94
94 def _create_repo(self, wire, create):
95 def _create_repo(self, wire, create):
95 repo_path = str_to_dulwich(wire['path'])
96 repo_path = str_to_dulwich(wire['path'])
96 return Repo(repo_path)
97 return Repo(repo_path)
97
98
98
99
99 class GitRemote(object):
100 class GitRemote(object):
100
101
101 def __init__(self, factory):
102 def __init__(self, factory):
102 self._factory = factory
103 self._factory = factory
103 self.peeled_ref_marker = '^{}'
104 self.peeled_ref_marker = '^{}'
104 self._bulk_methods = {
105 self._bulk_methods = {
105 "author": self.commit_attribute,
106 "author": self.commit_attribute,
106 "date": self.get_object_attrs,
107 "date": self.get_object_attrs,
107 "message": self.commit_attribute,
108 "message": self.commit_attribute,
108 "parents": self.commit_attribute,
109 "parents": self.commit_attribute,
109 "_commit": self.revision,
110 "_commit": self.revision,
110 }
111 }
111
112
112 def _wire_to_config(self, wire):
113 def _wire_to_config(self, wire):
113 if 'config' in wire:
114 if 'config' in wire:
114 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
115 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
115 return {}
116 return {}
116
117
117 def _assign_ref(self, wire, ref, commit_id):
118 def _assign_ref(self, wire, ref, commit_id):
118 repo = self._factory.repo(wire)
119 repo = self._factory.repo(wire)
119 repo[ref] = commit_id
120 repo[ref] = commit_id
120
121
122 def _remote_conf(self, config):
123 params = [
124 '-c', 'core.askpass=""',
125 ]
126 ssl_cert_dir = config.get('vcs_ssl_dir')
127 if ssl_cert_dir:
128 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
129 return params
130
121 @reraise_safe_exceptions
131 @reraise_safe_exceptions
122 def add_object(self, wire, content):
132 def add_object(self, wire, content):
123 repo = self._factory.repo(wire)
133 repo = self._factory.repo(wire)
124 blob = objects.Blob()
134 blob = objects.Blob()
125 blob.set_raw_string(content)
135 blob.set_raw_string(content)
126 repo.object_store.add_object(blob)
136 repo.object_store.add_object(blob)
127 return blob.id
137 return blob.id
128
138
129 @reraise_safe_exceptions
139 @reraise_safe_exceptions
130 def assert_correct_path(self, wire):
140 def assert_correct_path(self, wire):
131 path = wire.get('path')
141 path = wire.get('path')
132 try:
142 try:
133 self._factory.repo(wire)
143 self._factory.repo(wire)
134 except NotGitRepository as e:
144 except NotGitRepository as e:
135 tb = traceback.format_exc()
145 tb = traceback.format_exc()
136 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
146 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
137 return False
147 return False
138
148
139 return True
149 return True
140
150
141 @reraise_safe_exceptions
151 @reraise_safe_exceptions
142 def bare(self, wire):
152 def bare(self, wire):
143 repo = self._factory.repo(wire)
153 repo = self._factory.repo(wire)
144 return repo.bare
154 return repo.bare
145
155
146 @reraise_safe_exceptions
156 @reraise_safe_exceptions
147 def blob_as_pretty_string(self, wire, sha):
157 def blob_as_pretty_string(self, wire, sha):
148 repo = self._factory.repo(wire)
158 repo = self._factory.repo(wire)
149 return repo[sha].as_pretty_string()
159 return repo[sha].as_pretty_string()
150
160
151 @reraise_safe_exceptions
161 @reraise_safe_exceptions
152 def blob_raw_length(self, wire, sha):
162 def blob_raw_length(self, wire, sha):
153 repo = self._factory.repo(wire)
163 repo = self._factory.repo(wire)
154 blob = repo[sha]
164 blob = repo[sha]
155 return blob.raw_length()
165 return blob.raw_length()
156
166
157 def _parse_lfs_pointer(self, raw_content):
167 def _parse_lfs_pointer(self, raw_content):
158
168
159 spec_string = 'version https://git-lfs.github.com/spec'
169 spec_string = 'version https://git-lfs.github.com/spec'
160 if raw_content and raw_content.startswith(spec_string):
170 if raw_content and raw_content.startswith(spec_string):
161 pattern = re.compile(r"""
171 pattern = re.compile(r"""
162 (?:\n)?
172 (?:\n)?
163 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
173 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
164 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
174 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
165 ^size[ ](?P<oid_size>[0-9]+)\n
175 ^size[ ](?P<oid_size>[0-9]+)\n
166 (?:\n)?
176 (?:\n)?
167 """, re.VERBOSE | re.MULTILINE)
177 """, re.VERBOSE | re.MULTILINE)
168 match = pattern.match(raw_content)
178 match = pattern.match(raw_content)
169 if match:
179 if match:
170 return match.groupdict()
180 return match.groupdict()
171
181
172 return {}
182 return {}
173
183
174 @reraise_safe_exceptions
184 @reraise_safe_exceptions
175 def is_large_file(self, wire, sha):
185 def is_large_file(self, wire, sha):
176 repo = self._factory.repo(wire)
186 repo = self._factory.repo(wire)
177 blob = repo[sha]
187 blob = repo[sha]
178 return self._parse_lfs_pointer(blob.as_raw_string())
188 return self._parse_lfs_pointer(blob.as_raw_string())
179
189
180 @reraise_safe_exceptions
190 @reraise_safe_exceptions
181 def in_largefiles_store(self, wire, oid):
191 def in_largefiles_store(self, wire, oid):
182 repo = self._factory.repo(wire)
192 repo = self._factory.repo(wire)
183 conf = self._wire_to_config(wire)
193 conf = self._wire_to_config(wire)
184
194
185 store_location = conf.get('vcs_git_lfs_store_location')
195 store_location = conf.get('vcs_git_lfs_store_location')
186 if store_location:
196 if store_location:
187 repo_name = repo.path
197 repo_name = repo.path
188 store = LFSOidStore(
198 store = LFSOidStore(
189 oid=oid, repo=repo_name, store_location=store_location)
199 oid=oid, repo=repo_name, store_location=store_location)
190 return store.has_oid()
200 return store.has_oid()
191
201
192 return False
202 return False
193
203
194 @reraise_safe_exceptions
204 @reraise_safe_exceptions
195 def store_path(self, wire, oid):
205 def store_path(self, wire, oid):
196 repo = self._factory.repo(wire)
206 repo = self._factory.repo(wire)
197 conf = self._wire_to_config(wire)
207 conf = self._wire_to_config(wire)
198
208
199 store_location = conf.get('vcs_git_lfs_store_location')
209 store_location = conf.get('vcs_git_lfs_store_location')
200 if store_location:
210 if store_location:
201 repo_name = repo.path
211 repo_name = repo.path
202 store = LFSOidStore(
212 store = LFSOidStore(
203 oid=oid, repo=repo_name, store_location=store_location)
213 oid=oid, repo=repo_name, store_location=store_location)
204 return store.oid_path
214 return store.oid_path
205 raise ValueError('Unable to fetch oid with path {}'.format(oid))
215 raise ValueError('Unable to fetch oid with path {}'.format(oid))
206
216
207 @reraise_safe_exceptions
217 @reraise_safe_exceptions
208 def bulk_request(self, wire, rev, pre_load):
218 def bulk_request(self, wire, rev, pre_load):
209 result = {}
219 result = {}
210 for attr in pre_load:
220 for attr in pre_load:
211 try:
221 try:
212 method = self._bulk_methods[attr]
222 method = self._bulk_methods[attr]
213 args = [wire, rev]
223 args = [wire, rev]
214 if attr == "date":
224 if attr == "date":
215 args.extend(["commit_time", "commit_timezone"])
225 args.extend(["commit_time", "commit_timezone"])
216 elif attr in ["author", "message", "parents"]:
226 elif attr in ["author", "message", "parents"]:
217 args.append(attr)
227 args.append(attr)
218 result[attr] = method(*args)
228 result[attr] = method(*args)
219 except KeyError as e:
229 except KeyError as e:
220 raise exceptions.VcsException(e)(
230 raise exceptions.VcsException(e)(
221 "Unknown bulk attribute: %s" % attr)
231 "Unknown bulk attribute: %s" % attr)
222 return result
232 return result
223
233
224 def _build_opener(self, url):
234 def _build_opener(self, url):
225 handlers = []
235 handlers = []
226 url_obj = url_parser(url)
236 url_obj = url_parser(url)
227 _, authinfo = url_obj.authinfo()
237 _, authinfo = url_obj.authinfo()
228
238
229 if authinfo:
239 if authinfo:
230 # create a password manager
240 # create a password manager
231 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
241 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
232 passmgr.add_password(*authinfo)
242 passmgr.add_password(*authinfo)
233
243
234 handlers.extend((httpbasicauthhandler(passmgr),
244 handlers.extend((httpbasicauthhandler(passmgr),
235 httpdigestauthhandler(passmgr)))
245 httpdigestauthhandler(passmgr)))
236
246
237 return urllib2.build_opener(*handlers)
247 return urllib2.build_opener(*handlers)
238
248
239 @reraise_safe_exceptions
249 @reraise_safe_exceptions
240 def check_url(self, url, config):
250 def check_url(self, url, config):
241 url_obj = url_parser(url)
251 url_obj = url_parser(url)
242 test_uri, _ = url_obj.authinfo()
252 test_uri, _ = url_obj.authinfo()
243 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
253 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
244 url_obj.query = obfuscate_qs(url_obj.query)
254 url_obj.query = obfuscate_qs(url_obj.query)
245 cleaned_uri = str(url_obj)
255 cleaned_uri = str(url_obj)
246 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
256 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
247
257
248 if not test_uri.endswith('info/refs'):
258 if not test_uri.endswith('info/refs'):
249 test_uri = test_uri.rstrip('/') + '/info/refs'
259 test_uri = test_uri.rstrip('/') + '/info/refs'
250
260
251 o = self._build_opener(url)
261 o = self._build_opener(url)
252 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
262 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
253
263
254 q = {"service": 'git-upload-pack'}
264 q = {"service": 'git-upload-pack'}
255 qs = '?%s' % urllib.urlencode(q)
265 qs = '?%s' % urllib.urlencode(q)
256 cu = "%s%s" % (test_uri, qs)
266 cu = "%s%s" % (test_uri, qs)
257 req = urllib2.Request(cu, None, {})
267 req = urllib2.Request(cu, None, {})
258
268
259 try:
269 try:
260 log.debug("Trying to open URL %s", cleaned_uri)
270 log.debug("Trying to open URL %s", cleaned_uri)
261 resp = o.open(req)
271 resp = o.open(req)
262 if resp.code != 200:
272 if resp.code != 200:
263 raise exceptions.URLError()('Return Code is not 200')
273 raise exceptions.URLError()('Return Code is not 200')
264 except Exception as e:
274 except Exception as e:
265 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
275 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
266 # means it cannot be cloned
276 # means it cannot be cloned
267 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
277 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
268
278
269 # now detect if it's proper git repo
279 # now detect if it's proper git repo
270 gitdata = resp.read()
280 gitdata = resp.read()
271 if 'service=git-upload-pack' in gitdata:
281 if 'service=git-upload-pack' in gitdata:
272 pass
282 pass
273 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
283 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
274 # old style git can return some other format !
284 # old style git can return some other format !
275 pass
285 pass
276 else:
286 else:
277 raise exceptions.URLError()(
287 raise exceptions.URLError()(
278 "url [%s] does not look like an git" % (cleaned_uri,))
288 "url [%s] does not look like an git" % (cleaned_uri,))
279
289
280 return True
290 return True
281
291
282 @reraise_safe_exceptions
292 @reraise_safe_exceptions
283 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
293 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
284 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
294 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
285 remote_refs = self.pull(wire, url, apply_refs=False)
295 remote_refs = self.pull(wire, url, apply_refs=False)
286 repo = self._factory.repo(wire)
296 repo = self._factory.repo(wire)
287 if isinstance(valid_refs, list):
297 if isinstance(valid_refs, list):
288 valid_refs = tuple(valid_refs)
298 valid_refs = tuple(valid_refs)
289
299
290 for k in remote_refs:
300 for k in remote_refs:
291 # only parse heads/tags and skip so called deferred tags
301 # only parse heads/tags and skip so called deferred tags
292 if k.startswith(valid_refs) and not k.endswith(deferred):
302 if k.startswith(valid_refs) and not k.endswith(deferred):
293 repo[k] = remote_refs[k]
303 repo[k] = remote_refs[k]
294
304
295 if update_after_clone:
305 if update_after_clone:
296 # we want to checkout HEAD
306 # we want to checkout HEAD
297 repo["HEAD"] = remote_refs["HEAD"]
307 repo["HEAD"] = remote_refs["HEAD"]
298 index.build_index_from_tree(repo.path, repo.index_path(),
308 index.build_index_from_tree(repo.path, repo.index_path(),
299 repo.object_store, repo["HEAD"].tree)
309 repo.object_store, repo["HEAD"].tree)
300
310
301 # TODO: this is quite complex, check if that can be simplified
311 # TODO: this is quite complex, check if that can be simplified
302 @reraise_safe_exceptions
312 @reraise_safe_exceptions
303 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
313 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
304 repo = self._factory.repo(wire)
314 repo = self._factory.repo(wire)
305 object_store = repo.object_store
315 object_store = repo.object_store
306
316
307 # Create tree and populates it with blobs
317 # Create tree and populates it with blobs
308 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
318 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
309
319
310 for node in updated:
320 for node in updated:
311 # Compute subdirs if needed
321 # Compute subdirs if needed
312 dirpath, nodename = vcspath.split(node['path'])
322 dirpath, nodename = vcspath.split(node['path'])
313 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
323 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
314 parent = commit_tree
324 parent = commit_tree
315 ancestors = [('', parent)]
325 ancestors = [('', parent)]
316
326
317 # Tries to dig for the deepest existing tree
327 # Tries to dig for the deepest existing tree
318 while dirnames:
328 while dirnames:
319 curdir = dirnames.pop(0)
329 curdir = dirnames.pop(0)
320 try:
330 try:
321 dir_id = parent[curdir][1]
331 dir_id = parent[curdir][1]
322 except KeyError:
332 except KeyError:
323 # put curdir back into dirnames and stops
333 # put curdir back into dirnames and stops
324 dirnames.insert(0, curdir)
334 dirnames.insert(0, curdir)
325 break
335 break
326 else:
336 else:
327 # If found, updates parent
337 # If found, updates parent
328 parent = repo[dir_id]
338 parent = repo[dir_id]
329 ancestors.append((curdir, parent))
339 ancestors.append((curdir, parent))
330 # Now parent is deepest existing tree and we need to create
340 # Now parent is deepest existing tree and we need to create
331 # subtrees for dirnames (in reverse order)
341 # subtrees for dirnames (in reverse order)
332 # [this only applies for nodes from added]
342 # [this only applies for nodes from added]
333 new_trees = []
343 new_trees = []
334
344
335 blob = objects.Blob.from_string(node['content'])
345 blob = objects.Blob.from_string(node['content'])
336
346
337 if dirnames:
347 if dirnames:
338 # If there are trees which should be created we need to build
348 # If there are trees which should be created we need to build
339 # them now (in reverse order)
349 # them now (in reverse order)
340 reversed_dirnames = list(reversed(dirnames))
350 reversed_dirnames = list(reversed(dirnames))
341 curtree = objects.Tree()
351 curtree = objects.Tree()
342 curtree[node['node_path']] = node['mode'], blob.id
352 curtree[node['node_path']] = node['mode'], blob.id
343 new_trees.append(curtree)
353 new_trees.append(curtree)
344 for dirname in reversed_dirnames[:-1]:
354 for dirname in reversed_dirnames[:-1]:
345 newtree = objects.Tree()
355 newtree = objects.Tree()
346 newtree[dirname] = (DIR_STAT, curtree.id)
356 newtree[dirname] = (DIR_STAT, curtree.id)
347 new_trees.append(newtree)
357 new_trees.append(newtree)
348 curtree = newtree
358 curtree = newtree
349 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
359 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
350 else:
360 else:
351 parent.add(
361 parent.add(
352 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
362 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
353
363
354 new_trees.append(parent)
364 new_trees.append(parent)
355 # Update ancestors
365 # Update ancestors
356 reversed_ancestors = reversed(
366 reversed_ancestors = reversed(
357 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
367 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
358 for parent, tree, path in reversed_ancestors:
368 for parent, tree, path in reversed_ancestors:
359 parent[path] = (DIR_STAT, tree.id)
369 parent[path] = (DIR_STAT, tree.id)
360 object_store.add_object(tree)
370 object_store.add_object(tree)
361
371
362 object_store.add_object(blob)
372 object_store.add_object(blob)
363 for tree in new_trees:
373 for tree in new_trees:
364 object_store.add_object(tree)
374 object_store.add_object(tree)
365
375
366 for node_path in removed:
376 for node_path in removed:
367 paths = node_path.split('/')
377 paths = node_path.split('/')
368 tree = commit_tree
378 tree = commit_tree
369 trees = [tree]
379 trees = [tree]
370 # Traverse deep into the forest...
380 # Traverse deep into the forest...
371 for path in paths:
381 for path in paths:
372 try:
382 try:
373 obj = repo[tree[path][1]]
383 obj = repo[tree[path][1]]
374 if isinstance(obj, objects.Tree):
384 if isinstance(obj, objects.Tree):
375 trees.append(obj)
385 trees.append(obj)
376 tree = obj
386 tree = obj
377 except KeyError:
387 except KeyError:
378 break
388 break
379 # Cut down the blob and all rotten trees on the way back...
389 # Cut down the blob and all rotten trees on the way back...
380 for path, tree in reversed(zip(paths, trees)):
390 for path, tree in reversed(zip(paths, trees)):
381 del tree[path]
391 del tree[path]
382 if tree:
392 if tree:
383 # This tree still has elements - don't remove it or any
393 # This tree still has elements - don't remove it or any
384 # of it's parents
394 # of it's parents
385 break
395 break
386
396
387 object_store.add_object(commit_tree)
397 object_store.add_object(commit_tree)
388
398
389 # Create commit
399 # Create commit
390 commit = objects.Commit()
400 commit = objects.Commit()
391 commit.tree = commit_tree.id
401 commit.tree = commit_tree.id
392 for k, v in commit_data.iteritems():
402 for k, v in commit_data.iteritems():
393 setattr(commit, k, v)
403 setattr(commit, k, v)
394 object_store.add_object(commit)
404 object_store.add_object(commit)
395
405
396 ref = 'refs/heads/%s' % branch
406 ref = 'refs/heads/%s' % branch
397 repo.refs[ref] = commit.id
407 repo.refs[ref] = commit.id
398
408
399 return commit.id
409 return commit.id
400
410
401 @reraise_safe_exceptions
411 @reraise_safe_exceptions
402 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
412 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
403 if url != 'default' and '://' not in url:
413 if url != 'default' and '://' not in url:
404 client = LocalGitClient(url)
414 client = LocalGitClient(url)
405 else:
415 else:
406 url_obj = url_parser(url)
416 url_obj = url_parser(url)
407 o = self._build_opener(url)
417 o = self._build_opener(url)
408 url, _ = url_obj.authinfo()
418 url, _ = url_obj.authinfo()
409 client = HttpGitClient(base_url=url, opener=o)
419 client = HttpGitClient(base_url=url, opener=o)
410 repo = self._factory.repo(wire)
420 repo = self._factory.repo(wire)
411
421
412 determine_wants = repo.object_store.determine_wants_all
422 determine_wants = repo.object_store.determine_wants_all
413 if refs:
423 if refs:
414 def determine_wants_requested(references):
424 def determine_wants_requested(references):
415 return [references[r] for r in references if r in refs]
425 return [references[r] for r in references if r in refs]
416 determine_wants = determine_wants_requested
426 determine_wants = determine_wants_requested
417
427
418 try:
428 try:
419 remote_refs = client.fetch(
429 remote_refs = client.fetch(
420 path=url, target=repo, determine_wants=determine_wants)
430 path=url, target=repo, determine_wants=determine_wants)
421 except NotGitRepository as e:
431 except NotGitRepository as e:
422 log.warning(
432 log.warning(
423 'Trying to fetch from "%s" failed, not a Git repository.', url)
433 'Trying to fetch from "%s" failed, not a Git repository.', url)
424 # Exception can contain unicode which we convert
434 # Exception can contain unicode which we convert
425 raise exceptions.AbortException(e)(repr(e))
435 raise exceptions.AbortException(e)(repr(e))
426
436
427 # mikhail: client.fetch() returns all the remote refs, but fetches only
437 # mikhail: client.fetch() returns all the remote refs, but fetches only
428 # refs filtered by `determine_wants` function. We need to filter result
438 # refs filtered by `determine_wants` function. We need to filter result
429 # as well
439 # as well
430 if refs:
440 if refs:
431 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
441 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
432
442
433 if apply_refs:
443 if apply_refs:
434 # TODO: johbo: Needs proper test coverage with a git repository
444 # TODO: johbo: Needs proper test coverage with a git repository
435 # that contains a tag object, so that we would end up with
445 # that contains a tag object, so that we would end up with
436 # a peeled ref at this point.
446 # a peeled ref at this point.
437 for k in remote_refs:
447 for k in remote_refs:
438 if k.endswith(self.peeled_ref_marker):
448 if k.endswith(self.peeled_ref_marker):
439 log.debug("Skipping peeled reference %s", k)
449 log.debug("Skipping peeled reference %s", k)
440 continue
450 continue
441 repo[k] = remote_refs[k]
451 repo[k] = remote_refs[k]
442
452
443 if refs and not update_after:
453 if refs and not update_after:
444 # mikhail: explicitly set the head to the last ref.
454 # mikhail: explicitly set the head to the last ref.
445 repo['HEAD'] = remote_refs[refs[-1]]
455 repo['HEAD'] = remote_refs[refs[-1]]
446
456
447 if update_after:
457 if update_after:
448 # we want to checkout HEAD
458 # we want to checkout HEAD
449 repo["HEAD"] = remote_refs["HEAD"]
459 repo["HEAD"] = remote_refs["HEAD"]
450 index.build_index_from_tree(repo.path, repo.index_path(),
460 index.build_index_from_tree(repo.path, repo.index_path(),
451 repo.object_store, repo["HEAD"].tree)
461 repo.object_store, repo["HEAD"].tree)
452 return remote_refs
462 return remote_refs
453
463
454 @reraise_safe_exceptions
464 @reraise_safe_exceptions
455 def sync_fetch(self, wire, url, refs=None):
465 def sync_fetch(self, wire, url, refs=None):
456 repo = self._factory.repo(wire)
466 repo = self._factory.repo(wire)
457 if refs and not isinstance(refs, (list, tuple)):
467 if refs and not isinstance(refs, (list, tuple)):
458 refs = [refs]
468 refs = [refs]
459
469 config = self._wire_to_config(wire)
460 # get all remote refs we'll use to fetch later
470 # get all remote refs we'll use to fetch later
461 output, __ = self.run_git_command(
471 output, __ = self.run_git_command(
462 wire, ['ls-remote', url], fail_on_stderr=False,
472 wire, ['ls-remote', url], fail_on_stderr=False,
463 _copts=['-c', 'core.askpass=""'],
473 _copts=self._remote_conf(config),
464 extra_env={'GIT_TERMINAL_PROMPT': '0'})
474 extra_env={'GIT_TERMINAL_PROMPT': '0'})
465
475
466 remote_refs = collections.OrderedDict()
476 remote_refs = collections.OrderedDict()
467 fetch_refs = []
477 fetch_refs = []
468
478
469 for ref_line in output.splitlines():
479 for ref_line in output.splitlines():
470 sha, ref = ref_line.split('\t')
480 sha, ref = ref_line.split('\t')
471 sha = sha.strip()
481 sha = sha.strip()
472 if ref in remote_refs:
482 if ref in remote_refs:
473 # duplicate, skip
483 # duplicate, skip
474 continue
484 continue
475 if ref.endswith(self.peeled_ref_marker):
485 if ref.endswith(self.peeled_ref_marker):
476 log.debug("Skipping peeled reference %s", ref)
486 log.debug("Skipping peeled reference %s", ref)
477 continue
487 continue
478 # don't sync HEAD
488 # don't sync HEAD
479 if ref in ['HEAD']:
489 if ref in ['HEAD']:
480 continue
490 continue
481
491
482 remote_refs[ref] = sha
492 remote_refs[ref] = sha
483
493
484 if refs and sha in refs:
494 if refs and sha in refs:
485 # we filter fetch using our specified refs
495 # we filter fetch using our specified refs
486 fetch_refs.append('{}:{}'.format(ref, ref))
496 fetch_refs.append('{}:{}'.format(ref, ref))
487 elif not refs:
497 elif not refs:
488 fetch_refs.append('{}:{}'.format(ref, ref))
498 fetch_refs.append('{}:{}'.format(ref, ref))
489
499 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
490 if fetch_refs:
500 if fetch_refs:
501 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
502 fetch_refs_chunks = list(chunk)
503 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
491 _out, _err = self.run_git_command(
504 _out, _err = self.run_git_command(
492 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs,
505 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
493 fail_on_stderr=False,
506 fail_on_stderr=False,
494 _copts=['-c', 'core.askpass=""'],
507 _copts=self._remote_conf(config),
495 extra_env={'GIT_TERMINAL_PROMPT': '0'})
508 extra_env={'GIT_TERMINAL_PROMPT': '0'})
496
509
497 return remote_refs
510 return remote_refs
498
511
499 @reraise_safe_exceptions
512 @reraise_safe_exceptions
500 def sync_push(self, wire, url, refs=None):
513 def sync_push(self, wire, url, refs=None):
501 if not self.check_url(url, wire):
514 if not self.check_url(url, wire):
502 return
515 return
503
516 config = self._wire_to_config(wire)
504 repo = self._factory.repo(wire)
517 repo = self._factory.repo(wire)
505 self.run_git_command(
518 self.run_git_command(
506 wire, ['push', url, '--mirror'], fail_on_stderr=False,
519 wire, ['push', url, '--mirror'], fail_on_stderr=False,
507 _copts=['-c', 'core.askpass=""'],
520 _copts=self._remote_conf(config),
508 extra_env={'GIT_TERMINAL_PROMPT': '0'})
521 extra_env={'GIT_TERMINAL_PROMPT': '0'})
509
522
510 @reraise_safe_exceptions
523 @reraise_safe_exceptions
511 def get_remote_refs(self, wire, url):
524 def get_remote_refs(self, wire, url):
512 repo = Repo(url)
525 repo = Repo(url)
513 return repo.get_refs()
526 return repo.get_refs()
514
527
515 @reraise_safe_exceptions
528 @reraise_safe_exceptions
516 def get_description(self, wire):
529 def get_description(self, wire):
517 repo = self._factory.repo(wire)
530 repo = self._factory.repo(wire)
518 return repo.get_description()
531 return repo.get_description()
519
532
520 @reraise_safe_exceptions
533 @reraise_safe_exceptions
521 def get_missing_revs(self, wire, rev1, rev2, path2):
534 def get_missing_revs(self, wire, rev1, rev2, path2):
522 repo = self._factory.repo(wire)
535 repo = self._factory.repo(wire)
523 LocalGitClient(thin_packs=False).fetch(path2, repo)
536 LocalGitClient(thin_packs=False).fetch(path2, repo)
524
537
525 wire_remote = wire.copy()
538 wire_remote = wire.copy()
526 wire_remote['path'] = path2
539 wire_remote['path'] = path2
527 repo_remote = self._factory.repo(wire_remote)
540 repo_remote = self._factory.repo(wire_remote)
528 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
541 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
529
542
530 revs = [
543 revs = [
531 x.commit.id
544 x.commit.id
532 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
545 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
533 return revs
546 return revs
534
547
535 @reraise_safe_exceptions
548 @reraise_safe_exceptions
536 def get_object(self, wire, sha):
549 def get_object(self, wire, sha):
537 repo = self._factory.repo(wire)
550 repo = self._factory.repo(wire)
538 obj = repo.get_object(sha)
551 obj = repo.get_object(sha)
539 commit_id = obj.id
552 commit_id = obj.id
540
553
541 if isinstance(obj, Tag):
554 if isinstance(obj, Tag):
542 commit_id = obj.object[1]
555 commit_id = obj.object[1]
543
556
544 return {
557 return {
545 'id': obj.id,
558 'id': obj.id,
546 'type': obj.type_name,
559 'type': obj.type_name,
547 'commit_id': commit_id
560 'commit_id': commit_id
548 }
561 }
549
562
550 @reraise_safe_exceptions
563 @reraise_safe_exceptions
551 def get_object_attrs(self, wire, sha, *attrs):
564 def get_object_attrs(self, wire, sha, *attrs):
552 repo = self._factory.repo(wire)
565 repo = self._factory.repo(wire)
553 obj = repo.get_object(sha)
566 obj = repo.get_object(sha)
554 return list(getattr(obj, a) for a in attrs)
567 return list(getattr(obj, a) for a in attrs)
555
568
556 @reraise_safe_exceptions
569 @reraise_safe_exceptions
557 def get_refs(self, wire):
570 def get_refs(self, wire):
558 repo = self._factory.repo(wire)
571 repo = self._factory.repo(wire)
559 result = {}
572 result = {}
560 for ref, sha in repo.refs.as_dict().items():
573 for ref, sha in repo.refs.as_dict().items():
561 peeled_sha = repo.get_peeled(ref)
574 peeled_sha = repo.get_peeled(ref)
562 result[ref] = peeled_sha
575 result[ref] = peeled_sha
563 return result
576 return result
564
577
565 @reraise_safe_exceptions
578 @reraise_safe_exceptions
566 def get_refs_path(self, wire):
579 def get_refs_path(self, wire):
567 repo = self._factory.repo(wire)
580 repo = self._factory.repo(wire)
568 return repo.refs.path
581 return repo.refs.path
569
582
570 @reraise_safe_exceptions
583 @reraise_safe_exceptions
571 def head(self, wire, show_exc=True):
584 def head(self, wire, show_exc=True):
572 repo = self._factory.repo(wire)
585 repo = self._factory.repo(wire)
573 try:
586 try:
574 return repo.head()
587 return repo.head()
575 except Exception:
588 except Exception:
576 if show_exc:
589 if show_exc:
577 raise
590 raise
578
591
579 @reraise_safe_exceptions
592 @reraise_safe_exceptions
580 def init(self, wire):
593 def init(self, wire):
581 repo_path = str_to_dulwich(wire['path'])
594 repo_path = str_to_dulwich(wire['path'])
582 self.repo = Repo.init(repo_path)
595 self.repo = Repo.init(repo_path)
583
596
584 @reraise_safe_exceptions
597 @reraise_safe_exceptions
585 def init_bare(self, wire):
598 def init_bare(self, wire):
586 repo_path = str_to_dulwich(wire['path'])
599 repo_path = str_to_dulwich(wire['path'])
587 self.repo = Repo.init_bare(repo_path)
600 self.repo = Repo.init_bare(repo_path)
588
601
589 @reraise_safe_exceptions
602 @reraise_safe_exceptions
590 def revision(self, wire, rev):
603 def revision(self, wire, rev):
591 repo = self._factory.repo(wire)
604 repo = self._factory.repo(wire)
592 obj = repo[rev]
605 obj = repo[rev]
593 obj_data = {
606 obj_data = {
594 'id': obj.id,
607 'id': obj.id,
595 }
608 }
596 try:
609 try:
597 obj_data['tree'] = obj.tree
610 obj_data['tree'] = obj.tree
598 except AttributeError:
611 except AttributeError:
599 pass
612 pass
600 return obj_data
613 return obj_data
601
614
602 @reraise_safe_exceptions
615 @reraise_safe_exceptions
603 def commit_attribute(self, wire, rev, attr):
616 def commit_attribute(self, wire, rev, attr):
604 repo = self._factory.repo(wire)
617 repo = self._factory.repo(wire)
605 obj = repo[rev]
618 obj = repo[rev]
606 return getattr(obj, attr)
619 return getattr(obj, attr)
607
620
608 @reraise_safe_exceptions
621 @reraise_safe_exceptions
609 def set_refs(self, wire, key, value):
622 def set_refs(self, wire, key, value):
610 repo = self._factory.repo(wire)
623 repo = self._factory.repo(wire)
611 repo.refs[key] = value
624 repo.refs[key] = value
612
625
613 @reraise_safe_exceptions
626 @reraise_safe_exceptions
614 def remove_ref(self, wire, key):
627 def remove_ref(self, wire, key):
615 repo = self._factory.repo(wire)
628 repo = self._factory.repo(wire)
616 del repo.refs[key]
629 del repo.refs[key]
617
630
618 @reraise_safe_exceptions
631 @reraise_safe_exceptions
619 def tree_changes(self, wire, source_id, target_id):
632 def tree_changes(self, wire, source_id, target_id):
620 repo = self._factory.repo(wire)
633 repo = self._factory.repo(wire)
621 source = repo[source_id].tree if source_id else None
634 source = repo[source_id].tree if source_id else None
622 target = repo[target_id].tree
635 target = repo[target_id].tree
623 result = repo.object_store.tree_changes(source, target)
636 result = repo.object_store.tree_changes(source, target)
624 return list(result)
637 return list(result)
625
638
626 @reraise_safe_exceptions
639 @reraise_safe_exceptions
627 def tree_items(self, wire, tree_id):
640 def tree_items(self, wire, tree_id):
628 repo = self._factory.repo(wire)
641 repo = self._factory.repo(wire)
629 tree = repo[tree_id]
642 tree = repo[tree_id]
630
643
631 result = []
644 result = []
632 for item in tree.iteritems():
645 for item in tree.iteritems():
633 item_sha = item.sha
646 item_sha = item.sha
634 item_mode = item.mode
647 item_mode = item.mode
635
648
636 if FILE_MODE(item_mode) == GIT_LINK:
649 if FILE_MODE(item_mode) == GIT_LINK:
637 item_type = "link"
650 item_type = "link"
638 else:
651 else:
639 item_type = repo[item_sha].type_name
652 item_type = repo[item_sha].type_name
640
653
641 result.append((item.path, item_mode, item_sha, item_type))
654 result.append((item.path, item_mode, item_sha, item_type))
642 return result
655 return result
643
656
644 @reraise_safe_exceptions
657 @reraise_safe_exceptions
645 def update_server_info(self, wire):
658 def update_server_info(self, wire):
646 repo = self._factory.repo(wire)
659 repo = self._factory.repo(wire)
647 update_server_info(repo)
660 update_server_info(repo)
648
661
649 @reraise_safe_exceptions
662 @reraise_safe_exceptions
650 def discover_git_version(self):
663 def discover_git_version(self):
651 stdout, _ = self.run_git_command(
664 stdout, _ = self.run_git_command(
652 {}, ['--version'], _bare=True, _safe=True)
665 {}, ['--version'], _bare=True, _safe=True)
653 prefix = 'git version'
666 prefix = 'git version'
654 if stdout.startswith(prefix):
667 if stdout.startswith(prefix):
655 stdout = stdout[len(prefix):]
668 stdout = stdout[len(prefix):]
656 return stdout.strip()
669 return stdout.strip()
657
670
658 @reraise_safe_exceptions
671 @reraise_safe_exceptions
659 def run_git_command(self, wire, cmd, **opts):
672 def run_git_command(self, wire, cmd, **opts):
660 path = wire.get('path', None)
673 path = wire.get('path', None)
661
674
662 if path and os.path.isdir(path):
675 if path and os.path.isdir(path):
663 opts['cwd'] = path
676 opts['cwd'] = path
664
677
665 if '_bare' in opts:
678 if '_bare' in opts:
666 _copts = []
679 _copts = []
667 del opts['_bare']
680 del opts['_bare']
668 else:
681 else:
669 _copts = ['-c', 'core.quotepath=false', ]
682 _copts = ['-c', 'core.quotepath=false', ]
670 safe_call = False
683 safe_call = False
671 if '_safe' in opts:
684 if '_safe' in opts:
672 # no exc on failure
685 # no exc on failure
673 del opts['_safe']
686 del opts['_safe']
674 safe_call = True
687 safe_call = True
675
688
676 if '_copts' in opts:
689 if '_copts' in opts:
677 _copts.extend(opts['_copts'] or [])
690 _copts.extend(opts['_copts'] or [])
678 del opts['_copts']
691 del opts['_copts']
679
692
680 gitenv = os.environ.copy()
693 gitenv = os.environ.copy()
681 gitenv.update(opts.pop('extra_env', {}))
694 gitenv.update(opts.pop('extra_env', {}))
682 # need to clean fix GIT_DIR !
695 # need to clean fix GIT_DIR !
683 if 'GIT_DIR' in gitenv:
696 if 'GIT_DIR' in gitenv:
684 del gitenv['GIT_DIR']
697 del gitenv['GIT_DIR']
685 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
698 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
686 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
699 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
687
700
688 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
701 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
689 _opts = {'env': gitenv, 'shell': False}
702 _opts = {'env': gitenv, 'shell': False}
690
703
691 try:
704 try:
692 _opts.update(opts)
705 _opts.update(opts)
693 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
706 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
694
707
695 return ''.join(p), ''.join(p.error)
708 return ''.join(p), ''.join(p.error)
696 except (EnvironmentError, OSError) as err:
709 except (EnvironmentError, OSError) as err:
697 cmd = ' '.join(cmd) # human friendly CMD
710 cmd = ' '.join(cmd) # human friendly CMD
698 tb_err = ("Couldn't run git command (%s).\n"
711 tb_err = ("Couldn't run git command (%s).\n"
699 "Original error was:%s\n"
712 "Original error was:%s\n"
700 "Call options:%s\n"
713 "Call options:%s\n"
701 % (cmd, err, _opts))
714 % (cmd, err, _opts))
702 log.exception(tb_err)
715 log.exception(tb_err)
703 if safe_call:
716 if safe_call:
704 return '', err
717 return '', err
705 else:
718 else:
706 raise exceptions.VcsException()(tb_err)
719 raise exceptions.VcsException()(tb_err)
707
720
708 @reraise_safe_exceptions
721 @reraise_safe_exceptions
709 def install_hooks(self, wire, force=False):
722 def install_hooks(self, wire, force=False):
710 from vcsserver.hook_utils import install_git_hooks
723 from vcsserver.hook_utils import install_git_hooks
711 repo = self._factory.repo(wire)
724 repo = self._factory.repo(wire)
712 return install_git_hooks(repo.path, repo.bare, force_create=force)
725 return install_git_hooks(repo.path, repo.bare, force_create=force)
713
726
727 @reraise_safe_exceptions
728 def get_hooks_info(self, wire):
729 from vcsserver.hook_utils import (
730 get_git_pre_hook_version, get_git_post_hook_version)
731 repo = self._factory.repo(wire)
732 return {
733 'pre_version': get_git_pre_hook_version(repo.path, repo.bare),
734 'post_version': get_git_post_hook_version(repo.path, repo.bare),
735 }
736
714
737
715 def str_to_dulwich(value):
738 def str_to_dulwich(value):
716 """
739 """
717 Dulwich 0.10.1a requires `unicode` objects to be passed in.
740 Dulwich 0.10.1a requires `unicode` objects to be passed in.
718 """
741 """
719 return value.decode(settings.WIRE_ENCODING)
742 return value.decode(settings.WIRE_ENCODING)
@@ -1,19 +1,19 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
19 from app import create_app
19 from app import create_app
@@ -1,287 +1,287 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import re
18 import re
19 import logging
19 import logging
20 from wsgiref.util import FileWrapper
20 from wsgiref.util import FileWrapper
21
21
22 import simplejson as json
22 import simplejson as json
23 from pyramid.config import Configurator
23 from pyramid.config import Configurator
24 from pyramid.response import Response, FileIter
24 from pyramid.response import Response, FileIter
25 from pyramid.httpexceptions import (
25 from pyramid.httpexceptions import (
26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
27 HTTPUnprocessableEntity)
27 HTTPUnprocessableEntity)
28
28
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
31 from vcsserver.utils import safe_int
31 from vcsserver.utils import safe_int
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
38
38
39
39
40 def write_response_error(http_exception, text=None):
40 def write_response_error(http_exception, text=None):
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
42 _exception = http_exception(content_type=content_type)
42 _exception = http_exception(content_type=content_type)
43 _exception.content_type = content_type
43 _exception.content_type = content_type
44 if text:
44 if text:
45 _exception.body = json.dumps({'message': text})
45 _exception.body = json.dumps({'message': text})
46 log.debug('LFS: writing response of type %s to client with text:%s',
46 log.debug('LFS: writing response of type %s to client with text:%s',
47 http_exception, text)
47 http_exception, text)
48 return _exception
48 return _exception
49
49
50
50
51 class AuthHeaderRequired(object):
51 class AuthHeaderRequired(object):
52 """
52 """
53 Decorator to check if request has proper auth-header
53 Decorator to check if request has proper auth-header
54 """
54 """
55
55
56 def __call__(self, func):
56 def __call__(self, func):
57 return get_cython_compat_decorator(self.__wrapper, func)
57 return get_cython_compat_decorator(self.__wrapper, func)
58
58
59 def __wrapper(self, func, *fargs, **fkwargs):
59 def __wrapper(self, func, *fargs, **fkwargs):
60 request = fargs[1]
60 request = fargs[1]
61 auth = request.authorization
61 auth = request.authorization
62 if not auth:
62 if not auth:
63 return write_response_error(HTTPForbidden)
63 return write_response_error(HTTPForbidden)
64 return func(*fargs[1:], **fkwargs)
64 return func(*fargs[1:], **fkwargs)
65
65
66
66
67 # views
67 # views
68
68
69 def lfs_objects(request):
69 def lfs_objects(request):
70 # indicate not supported, V1 API
70 # indicate not supported, V1 API
71 log.warning('LFS: v1 api not supported, reporting it back to client')
71 log.warning('LFS: v1 api not supported, reporting it back to client')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
73
73
74
74
75 @AuthHeaderRequired()
75 @AuthHeaderRequired()
76 def lfs_objects_batch(request):
76 def lfs_objects_batch(request):
77 """
77 """
78 The client sends the following information to the Batch endpoint to transfer some objects:
78 The client sends the following information to the Batch endpoint to transfer some objects:
79
79
80 operation - Should be download or upload.
80 operation - Should be download or upload.
81 transfers - An optional Array of String identifiers for transfer
81 transfers - An optional Array of String identifiers for transfer
82 adapters that the client has configured. If omitted, the basic
82 adapters that the client has configured. If omitted, the basic
83 transfer adapter MUST be assumed by the server.
83 transfer adapter MUST be assumed by the server.
84 objects - An Array of objects to download.
84 objects - An Array of objects to download.
85 oid - String OID of the LFS object.
85 oid - String OID of the LFS object.
86 size - Integer byte size of the LFS object. Must be at least zero.
86 size - Integer byte size of the LFS object. Must be at least zero.
87 """
87 """
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
89 auth = request.authorization
89 auth = request.authorization
90 repo = request.matchdict.get('repo')
90 repo = request.matchdict.get('repo')
91 data = request.json
91 data = request.json
92 operation = data.get('operation')
92 operation = data.get('operation')
93 if operation not in ('download', 'upload'):
93 if operation not in ('download', 'upload'):
94 log.debug('LFS: unsupported operation:%s', operation)
94 log.debug('LFS: unsupported operation:%s', operation)
95 return write_response_error(
95 return write_response_error(
96 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
96 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
97
97
98 if 'objects' not in data:
98 if 'objects' not in data:
99 log.debug('LFS: missing objects data')
99 log.debug('LFS: missing objects data')
100 return write_response_error(
100 return write_response_error(
101 HTTPBadRequest, 'missing objects data')
101 HTTPBadRequest, 'missing objects data')
102
102
103 log.debug('LFS: handling operation of type: %s', operation)
103 log.debug('LFS: handling operation of type: %s', operation)
104
104
105 objects = []
105 objects = []
106 for o in data['objects']:
106 for o in data['objects']:
107 try:
107 try:
108 oid = o['oid']
108 oid = o['oid']
109 obj_size = o['size']
109 obj_size = o['size']
110 except KeyError:
110 except KeyError:
111 log.exception('LFS, failed to extract data')
111 log.exception('LFS, failed to extract data')
112 return write_response_error(
112 return write_response_error(
113 HTTPBadRequest, 'unsupported data in objects')
113 HTTPBadRequest, 'unsupported data in objects')
114
114
115 obj_data = {'oid': oid}
115 obj_data = {'oid': oid}
116
116
117 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
117 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
118 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
118 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
119 store = LFSOidStore(
119 store = LFSOidStore(
120 oid, repo, store_location=request.registry.git_lfs_store_path)
120 oid, repo, store_location=request.registry.git_lfs_store_path)
121 handler = OidHandler(
121 handler = OidHandler(
122 store, repo, auth, oid, obj_size, obj_data,
122 store, repo, auth, oid, obj_size, obj_data,
123 obj_href, obj_verify_href)
123 obj_href, obj_verify_href)
124
124
125 # this verifies also OIDs
125 # this verifies also OIDs
126 actions, errors = handler.exec_operation(operation)
126 actions, errors = handler.exec_operation(operation)
127 if errors:
127 if errors:
128 log.warning('LFS: got following errors: %s', errors)
128 log.warning('LFS: got following errors: %s', errors)
129 obj_data['errors'] = errors
129 obj_data['errors'] = errors
130
130
131 if actions:
131 if actions:
132 obj_data['actions'] = actions
132 obj_data['actions'] = actions
133
133
134 obj_data['size'] = obj_size
134 obj_data['size'] = obj_size
135 obj_data['authenticated'] = True
135 obj_data['authenticated'] = True
136 objects.append(obj_data)
136 objects.append(obj_data)
137
137
138 result = {'objects': objects, 'transfer': 'basic'}
138 result = {'objects': objects, 'transfer': 'basic'}
139 log.debug('LFS Response %s', safe_result(result))
139 log.debug('LFS Response %s', safe_result(result))
140
140
141 return result
141 return result
142
142
143
143
144 def lfs_objects_oid_upload(request):
144 def lfs_objects_oid_upload(request):
145 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
145 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
146 repo = request.matchdict.get('repo')
146 repo = request.matchdict.get('repo')
147 oid = request.matchdict.get('oid')
147 oid = request.matchdict.get('oid')
148 store = LFSOidStore(
148 store = LFSOidStore(
149 oid, repo, store_location=request.registry.git_lfs_store_path)
149 oid, repo, store_location=request.registry.git_lfs_store_path)
150 engine = store.get_engine(mode='wb')
150 engine = store.get_engine(mode='wb')
151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
152
152
153 body = request.environ['wsgi.input']
153 body = request.environ['wsgi.input']
154
154
155 with engine as f:
155 with engine as f:
156 blksize = 64 * 1024 # 64kb
156 blksize = 64 * 1024 # 64kb
157 while True:
157 while True:
158 # read in chunks as stream comes in from Gunicorn
158 # read in chunks as stream comes in from Gunicorn
159 # this is a specific Gunicorn support function.
159 # this is a specific Gunicorn support function.
160 # might work differently on waitress
160 # might work differently on waitress
161 chunk = body.read(blksize)
161 chunk = body.read(blksize)
162 if not chunk:
162 if not chunk:
163 break
163 break
164 f.write(chunk)
164 f.write(chunk)
165
165
166 return {'upload': 'ok'}
166 return {'upload': 'ok'}
167
167
168
168
169 def lfs_objects_oid_download(request):
169 def lfs_objects_oid_download(request):
170 repo = request.matchdict.get('repo')
170 repo = request.matchdict.get('repo')
171 oid = request.matchdict.get('oid')
171 oid = request.matchdict.get('oid')
172
172
173 store = LFSOidStore(
173 store = LFSOidStore(
174 oid, repo, store_location=request.registry.git_lfs_store_path)
174 oid, repo, store_location=request.registry.git_lfs_store_path)
175 if not store.has_oid():
175 if not store.has_oid():
176 log.debug('LFS: oid %s does not exists in store', oid)
176 log.debug('LFS: oid %s does not exists in store', oid)
177 return write_response_error(
177 return write_response_error(
178 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
178 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
179
179
180 # TODO(marcink): support range header ?
180 # TODO(marcink): support range header ?
181 # Range: bytes=0-, `bytes=(\d+)\-.*`
181 # Range: bytes=0-, `bytes=(\d+)\-.*`
182
182
183 f = open(store.oid_path, 'rb')
183 f = open(store.oid_path, 'rb')
184 response = Response(
184 response = Response(
185 content_type='application/octet-stream', app_iter=FileIter(f))
185 content_type='application/octet-stream', app_iter=FileIter(f))
186 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
186 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
187 return response
187 return response
188
188
189
189
190 def lfs_objects_verify(request):
190 def lfs_objects_verify(request):
191 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
191 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
192 repo = request.matchdict.get('repo')
192 repo = request.matchdict.get('repo')
193
193
194 data = request.json
194 data = request.json
195 oid = data.get('oid')
195 oid = data.get('oid')
196 size = safe_int(data.get('size'))
196 size = safe_int(data.get('size'))
197
197
198 if not (oid and size):
198 if not (oid and size):
199 return write_response_error(
199 return write_response_error(
200 HTTPBadRequest, 'missing oid and size in request data')
200 HTTPBadRequest, 'missing oid and size in request data')
201
201
202 store = LFSOidStore(
202 store = LFSOidStore(
203 oid, repo, store_location=request.registry.git_lfs_store_path)
203 oid, repo, store_location=request.registry.git_lfs_store_path)
204 if not store.has_oid():
204 if not store.has_oid():
205 log.debug('LFS: oid %s does not exists in store', oid)
205 log.debug('LFS: oid %s does not exists in store', oid)
206 return write_response_error(
206 return write_response_error(
207 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
207 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
208
208
209 store_size = store.size_oid()
209 store_size = store.size_oid()
210 if store_size != size:
210 if store_size != size:
211 msg = 'requested file size mismatch store size:%s requested:%s' % (
211 msg = 'requested file size mismatch store size:%s requested:%s' % (
212 store_size, size)
212 store_size, size)
213 return write_response_error(
213 return write_response_error(
214 HTTPUnprocessableEntity, msg)
214 HTTPUnprocessableEntity, msg)
215
215
216 return {'message': {'size': 'ok', 'in_store': 'ok'}}
216 return {'message': {'size': 'ok', 'in_store': 'ok'}}
217
217
218
218
219 def lfs_objects_lock(request):
219 def lfs_objects_lock(request):
220 return write_response_error(
220 return write_response_error(
221 HTTPNotImplemented, 'GIT LFS locking api not supported')
221 HTTPNotImplemented, 'GIT LFS locking api not supported')
222
222
223
223
224 def not_found(request):
224 def not_found(request):
225 return write_response_error(
225 return write_response_error(
226 HTTPNotFound, 'request path not found')
226 HTTPNotFound, 'request path not found')
227
227
228
228
229 def lfs_disabled(request):
229 def lfs_disabled(request):
230 return write_response_error(
230 return write_response_error(
231 HTTPNotImplemented, 'GIT LFS disabled for this repo')
231 HTTPNotImplemented, 'GIT LFS disabled for this repo')
232
232
233
233
234 def git_lfs_app(config):
234 def git_lfs_app(config):
235
235
236 # v1 API deprecation endpoint
236 # v1 API deprecation endpoint
237 config.add_route('lfs_objects',
237 config.add_route('lfs_objects',
238 '/{repo:.*?[^/]}/info/lfs/objects')
238 '/{repo:.*?[^/]}/info/lfs/objects')
239 config.add_view(lfs_objects, route_name='lfs_objects',
239 config.add_view(lfs_objects, route_name='lfs_objects',
240 request_method='POST', renderer='json')
240 request_method='POST', renderer='json')
241
241
242 # locking API
242 # locking API
243 config.add_route('lfs_objects_lock',
243 config.add_route('lfs_objects_lock',
244 '/{repo:.*?[^/]}/info/lfs/locks')
244 '/{repo:.*?[^/]}/info/lfs/locks')
245 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
245 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
246 request_method=('POST', 'GET'), renderer='json')
246 request_method=('POST', 'GET'), renderer='json')
247
247
248 config.add_route('lfs_objects_lock_verify',
248 config.add_route('lfs_objects_lock_verify',
249 '/{repo:.*?[^/]}/info/lfs/locks/verify')
249 '/{repo:.*?[^/]}/info/lfs/locks/verify')
250 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
250 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
251 request_method=('POST', 'GET'), renderer='json')
251 request_method=('POST', 'GET'), renderer='json')
252
252
253 # batch API
253 # batch API
254 config.add_route('lfs_objects_batch',
254 config.add_route('lfs_objects_batch',
255 '/{repo:.*?[^/]}/info/lfs/objects/batch')
255 '/{repo:.*?[^/]}/info/lfs/objects/batch')
256 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
256 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
257 request_method='POST', renderer='json')
257 request_method='POST', renderer='json')
258
258
259 # oid upload/download API
259 # oid upload/download API
260 config.add_route('lfs_objects_oid',
260 config.add_route('lfs_objects_oid',
261 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
261 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
262 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
262 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
263 request_method='PUT', renderer='json')
263 request_method='PUT', renderer='json')
264 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
264 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
265 request_method='GET', renderer='json')
265 request_method='GET', renderer='json')
266
266
267 # verification API
267 # verification API
268 config.add_route('lfs_objects_verify',
268 config.add_route('lfs_objects_verify',
269 '/{repo:.*?[^/]}/info/lfs/verify')
269 '/{repo:.*?[^/]}/info/lfs/verify')
270 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
270 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
271 request_method='POST', renderer='json')
271 request_method='POST', renderer='json')
272
272
273 # not found handler for API
273 # not found handler for API
274 config.add_notfound_view(not_found, renderer='json')
274 config.add_notfound_view(not_found, renderer='json')
275
275
276
276
277 def create_app(git_lfs_enabled, git_lfs_store_path):
277 def create_app(git_lfs_enabled, git_lfs_store_path):
278 config = Configurator()
278 config = Configurator()
279 if git_lfs_enabled:
279 if git_lfs_enabled:
280 config.include(git_lfs_app)
280 config.include(git_lfs_app)
281 config.registry.git_lfs_store_path = git_lfs_store_path
281 config.registry.git_lfs_store_path = git_lfs_store_path
282 else:
282 else:
283 # not found handler for API, reporting disabled LFS support
283 # not found handler for API, reporting disabled LFS support
284 config.add_notfound_view(lfs_disabled, renderer='json')
284 config.add_notfound_view(lfs_disabled, renderer='json')
285
285
286 app = config.make_wsgi_app()
286 app = config.make_wsgi_app()
287 return app
287 return app
@@ -1,175 +1,175 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import shutil
19 import shutil
20 import logging
20 import logging
21 from collections import OrderedDict
21 from collections import OrderedDict
22
22
23 log = logging.getLogger(__name__)
23 log = logging.getLogger(__name__)
24
24
25
25
26 class OidHandler(object):
26 class OidHandler(object):
27
27
28 def __init__(self, store, repo_name, auth, oid, obj_size, obj_data, obj_href,
28 def __init__(self, store, repo_name, auth, oid, obj_size, obj_data, obj_href,
29 obj_verify_href=None):
29 obj_verify_href=None):
30 self.current_store = store
30 self.current_store = store
31 self.repo_name = repo_name
31 self.repo_name = repo_name
32 self.auth = auth
32 self.auth = auth
33 self.oid = oid
33 self.oid = oid
34 self.obj_size = obj_size
34 self.obj_size = obj_size
35 self.obj_data = obj_data
35 self.obj_data = obj_data
36 self.obj_href = obj_href
36 self.obj_href = obj_href
37 self.obj_verify_href = obj_verify_href
37 self.obj_verify_href = obj_verify_href
38
38
39 def get_store(self, mode=None):
39 def get_store(self, mode=None):
40 return self.current_store
40 return self.current_store
41
41
42 def get_auth(self):
42 def get_auth(self):
43 """returns auth header for re-use in upload/download"""
43 """returns auth header for re-use in upload/download"""
44 return " ".join(self.auth)
44 return " ".join(self.auth)
45
45
46 def download(self):
46 def download(self):
47
47
48 store = self.get_store()
48 store = self.get_store()
49 response = None
49 response = None
50 has_errors = None
50 has_errors = None
51
51
52 if not store.has_oid():
52 if not store.has_oid():
53 # error reply back to client that something is wrong with dl
53 # error reply back to client that something is wrong with dl
54 err_msg = 'object: {} does not exist in store'.format(store.oid)
54 err_msg = 'object: {} does not exist in store'.format(store.oid)
55 has_errors = OrderedDict(
55 has_errors = OrderedDict(
56 error=OrderedDict(
56 error=OrderedDict(
57 code=404,
57 code=404,
58 message=err_msg
58 message=err_msg
59 )
59 )
60 )
60 )
61
61
62 download_action = OrderedDict(
62 download_action = OrderedDict(
63 href=self.obj_href,
63 href=self.obj_href,
64 header=OrderedDict([("Authorization", self.get_auth())])
64 header=OrderedDict([("Authorization", self.get_auth())])
65 )
65 )
66 if not has_errors:
66 if not has_errors:
67 response = OrderedDict(download=download_action)
67 response = OrderedDict(download=download_action)
68 return response, has_errors
68 return response, has_errors
69
69
70 def upload(self, skip_existing=True):
70 def upload(self, skip_existing=True):
71 """
71 """
72 Write upload action for git-lfs server
72 Write upload action for git-lfs server
73 """
73 """
74
74
75 store = self.get_store()
75 store = self.get_store()
76 response = None
76 response = None
77 has_errors = None
77 has_errors = None
78
78
79 # verify if we have the OID before, if we do, reply with empty
79 # verify if we have the OID before, if we do, reply with empty
80 if store.has_oid():
80 if store.has_oid():
81 log.debug('LFS: store already has oid %s', store.oid)
81 log.debug('LFS: store already has oid %s', store.oid)
82
82
83 # validate size
83 # validate size
84 store_size = store.size_oid()
84 store_size = store.size_oid()
85 size_match = store_size == self.obj_size
85 size_match = store_size == self.obj_size
86 if not size_match:
86 if not size_match:
87 log.warning(
87 log.warning(
88 'LFS: size mismatch for oid:%s, in store:%s expected: %s',
88 'LFS: size mismatch for oid:%s, in store:%s expected: %s',
89 self.oid, store_size, self.obj_size)
89 self.oid, store_size, self.obj_size)
90 elif skip_existing:
90 elif skip_existing:
91 log.debug('LFS: skipping further action as oid is existing')
91 log.debug('LFS: skipping further action as oid is existing')
92 return response, has_errors
92 return response, has_errors
93
93
94 chunked = ("Transfer-Encoding", "chunked")
94 chunked = ("Transfer-Encoding", "chunked")
95 upload_action = OrderedDict(
95 upload_action = OrderedDict(
96 href=self.obj_href,
96 href=self.obj_href,
97 header=OrderedDict([("Authorization", self.get_auth()), chunked])
97 header=OrderedDict([("Authorization", self.get_auth()), chunked])
98 )
98 )
99 if not has_errors:
99 if not has_errors:
100 response = OrderedDict(upload=upload_action)
100 response = OrderedDict(upload=upload_action)
101 # if specified in handler, return the verification endpoint
101 # if specified in handler, return the verification endpoint
102 if self.obj_verify_href:
102 if self.obj_verify_href:
103 verify_action = OrderedDict(
103 verify_action = OrderedDict(
104 href=self.obj_verify_href,
104 href=self.obj_verify_href,
105 header=OrderedDict([("Authorization", self.get_auth())])
105 header=OrderedDict([("Authorization", self.get_auth())])
106 )
106 )
107 response['verify'] = verify_action
107 response['verify'] = verify_action
108 return response, has_errors
108 return response, has_errors
109
109
110 def exec_operation(self, operation, *args, **kwargs):
110 def exec_operation(self, operation, *args, **kwargs):
111 handler = getattr(self, operation)
111 handler = getattr(self, operation)
112 log.debug('LFS: handling request using %s handler', handler)
112 log.debug('LFS: handling request using %s handler', handler)
113 return handler(*args, **kwargs)
113 return handler(*args, **kwargs)
114
114
115
115
116 class LFSOidStore(object):
116 class LFSOidStore(object):
117
117
118 def __init__(self, oid, repo, store_location=None):
118 def __init__(self, oid, repo, store_location=None):
119 self.oid = oid
119 self.oid = oid
120 self.repo = repo
120 self.repo = repo
121 self.store_path = store_location or self.get_default_store()
121 self.store_path = store_location or self.get_default_store()
122 self.tmp_oid_path = os.path.join(self.store_path, oid + '.tmp')
122 self.tmp_oid_path = os.path.join(self.store_path, oid + '.tmp')
123 self.oid_path = os.path.join(self.store_path, oid)
123 self.oid_path = os.path.join(self.store_path, oid)
124 self.fd = None
124 self.fd = None
125
125
126 def get_engine(self, mode):
126 def get_engine(self, mode):
127 """
127 """
128 engine = .get_engine(mode='wb')
128 engine = .get_engine(mode='wb')
129 with engine as f:
129 with engine as f:
130 f.write('...')
130 f.write('...')
131 """
131 """
132
132
133 class StoreEngine(object):
133 class StoreEngine(object):
134 def __init__(self, mode, store_path, oid_path, tmp_oid_path):
134 def __init__(self, mode, store_path, oid_path, tmp_oid_path):
135 self.mode = mode
135 self.mode = mode
136 self.store_path = store_path
136 self.store_path = store_path
137 self.oid_path = oid_path
137 self.oid_path = oid_path
138 self.tmp_oid_path = tmp_oid_path
138 self.tmp_oid_path = tmp_oid_path
139
139
140 def __enter__(self):
140 def __enter__(self):
141 if not os.path.isdir(self.store_path):
141 if not os.path.isdir(self.store_path):
142 os.makedirs(self.store_path)
142 os.makedirs(self.store_path)
143
143
144 # TODO(marcink): maybe write metadata here with size/oid ?
144 # TODO(marcink): maybe write metadata here with size/oid ?
145 fd = open(self.tmp_oid_path, self.mode)
145 fd = open(self.tmp_oid_path, self.mode)
146 self.fd = fd
146 self.fd = fd
147 return fd
147 return fd
148
148
149 def __exit__(self, exc_type, exc_value, traceback):
149 def __exit__(self, exc_type, exc_value, traceback):
150 # close tmp file, and rename to final destination
150 # close tmp file, and rename to final destination
151 self.fd.close()
151 self.fd.close()
152 shutil.move(self.tmp_oid_path, self.oid_path)
152 shutil.move(self.tmp_oid_path, self.oid_path)
153
153
154 return StoreEngine(
154 return StoreEngine(
155 mode, self.store_path, self.oid_path, self.tmp_oid_path)
155 mode, self.store_path, self.oid_path, self.tmp_oid_path)
156
156
157 def get_default_store(self):
157 def get_default_store(self):
158 """
158 """
159 Default store, consistent with defaults of Mercurial large files store
159 Default store, consistent with defaults of Mercurial large files store
160 which is /home/username/.cache/largefiles
160 which is /home/username/.cache/largefiles
161 """
161 """
162 user_home = os.path.expanduser("~")
162 user_home = os.path.expanduser("~")
163 return os.path.join(user_home, '.cache', 'lfs-store')
163 return os.path.join(user_home, '.cache', 'lfs-store')
164
164
165 def has_oid(self):
165 def has_oid(self):
166 return os.path.exists(os.path.join(self.store_path, self.oid))
166 return os.path.exists(os.path.join(self.store_path, self.oid))
167
167
168 def size_oid(self):
168 def size_oid(self):
169 size = -1
169 size = -1
170
170
171 if self.has_oid():
171 if self.has_oid():
172 oid = os.path.join(self.store_path, self.oid)
172 oid = os.path.join(self.store_path, self.oid)
173 size = os.stat(oid).st_size
173 size = os.stat(oid).st_size
174
174
175 return size
175 return size
@@ -1,16 +1,16 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,239 +1,239 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import pytest
19 import pytest
20 from webtest.app import TestApp as WebObTestApp
20 from webtest.app import TestApp as WebObTestApp
21 import simplejson as json
21 import simplejson as json
22
22
23 from vcsserver.git_lfs.app import create_app
23 from vcsserver.git_lfs.app import create_app
24
24
25
25
26 @pytest.fixture(scope='function')
26 @pytest.fixture(scope='function')
27 def git_lfs_app(tmpdir):
27 def git_lfs_app(tmpdir):
28 custom_app = WebObTestApp(create_app(
28 custom_app = WebObTestApp(create_app(
29 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir)))
29 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir)))
30 custom_app._store = str(tmpdir)
30 custom_app._store = str(tmpdir)
31 return custom_app
31 return custom_app
32
32
33
33
34 @pytest.fixture()
34 @pytest.fixture()
35 def http_auth():
35 def http_auth():
36 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
36 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
37
37
38
38
39 class TestLFSApplication(object):
39 class TestLFSApplication(object):
40
40
41 def test_app_wrong_path(self, git_lfs_app):
41 def test_app_wrong_path(self, git_lfs_app):
42 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
42 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
43
43
44 def test_app_deprecated_endpoint(self, git_lfs_app):
44 def test_app_deprecated_endpoint(self, git_lfs_app):
45 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
45 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
46 assert response.status_code == 501
46 assert response.status_code == 501
47 assert json.loads(response.text) == {u'message': u'LFS: v1 api not supported'}
47 assert json.loads(response.text) == {u'message': u'LFS: v1 api not supported'}
48
48
49 def test_app_lock_verify_api_not_available(self, git_lfs_app):
49 def test_app_lock_verify_api_not_available(self, git_lfs_app):
50 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
50 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
51 assert response.status_code == 501
51 assert response.status_code == 501
52 assert json.loads(response.text) == {
52 assert json.loads(response.text) == {
53 u'message': u'GIT LFS locking api not supported'}
53 u'message': u'GIT LFS locking api not supported'}
54
54
55 def test_app_lock_api_not_available(self, git_lfs_app):
55 def test_app_lock_api_not_available(self, git_lfs_app):
56 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
56 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
57 assert response.status_code == 501
57 assert response.status_code == 501
58 assert json.loads(response.text) == {
58 assert json.loads(response.text) == {
59 u'message': u'GIT LFS locking api not supported'}
59 u'message': u'GIT LFS locking api not supported'}
60
60
61 def test_app_batch_api_missing_auth(self, git_lfs_app,):
61 def test_app_batch_api_missing_auth(self, git_lfs_app,):
62 git_lfs_app.post_json(
62 git_lfs_app.post_json(
63 '/repo/info/lfs/objects/batch', params={}, status=403)
63 '/repo/info/lfs/objects/batch', params={}, status=403)
64
64
65 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
65 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
66 response = git_lfs_app.post_json(
66 response = git_lfs_app.post_json(
67 '/repo/info/lfs/objects/batch', params={}, status=400,
67 '/repo/info/lfs/objects/batch', params={}, status=400,
68 extra_environ=http_auth)
68 extra_environ=http_auth)
69 assert json.loads(response.text) == {
69 assert json.loads(response.text) == {
70 u'message': u'unsupported operation mode: `None`'}
70 u'message': u'unsupported operation mode: `None`'}
71
71
72 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
72 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
73 response = git_lfs_app.post_json(
73 response = git_lfs_app.post_json(
74 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
74 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
75 status=400, extra_environ=http_auth)
75 status=400, extra_environ=http_auth)
76 assert json.loads(response.text) == {
76 assert json.loads(response.text) == {
77 u'message': u'missing objects data'}
77 u'message': u'missing objects data'}
78
78
79 def test_app_batch_api_unsupported_data_in_objects(
79 def test_app_batch_api_unsupported_data_in_objects(
80 self, git_lfs_app, http_auth):
80 self, git_lfs_app, http_auth):
81 params = {'operation': 'download',
81 params = {'operation': 'download',
82 'objects': [{}]}
82 'objects': [{}]}
83 response = git_lfs_app.post_json(
83 response = git_lfs_app.post_json(
84 '/repo/info/lfs/objects/batch', params=params, status=400,
84 '/repo/info/lfs/objects/batch', params=params, status=400,
85 extra_environ=http_auth)
85 extra_environ=http_auth)
86 assert json.loads(response.text) == {
86 assert json.loads(response.text) == {
87 u'message': u'unsupported data in objects'}
87 u'message': u'unsupported data in objects'}
88
88
89 def test_app_batch_api_download_missing_object(
89 def test_app_batch_api_download_missing_object(
90 self, git_lfs_app, http_auth):
90 self, git_lfs_app, http_auth):
91 params = {'operation': 'download',
91 params = {'operation': 'download',
92 'objects': [{'oid': '123', 'size': '1024'}]}
92 'objects': [{'oid': '123', 'size': '1024'}]}
93 response = git_lfs_app.post_json(
93 response = git_lfs_app.post_json(
94 '/repo/info/lfs/objects/batch', params=params,
94 '/repo/info/lfs/objects/batch', params=params,
95 extra_environ=http_auth)
95 extra_environ=http_auth)
96
96
97 expected_objects = [
97 expected_objects = [
98 {u'authenticated': True,
98 {u'authenticated': True,
99 u'errors': {u'error': {
99 u'errors': {u'error': {
100 u'code': 404,
100 u'code': 404,
101 u'message': u'object: 123 does not exist in store'}},
101 u'message': u'object: 123 does not exist in store'}},
102 u'oid': u'123',
102 u'oid': u'123',
103 u'size': u'1024'}
103 u'size': u'1024'}
104 ]
104 ]
105 assert json.loads(response.text) == {
105 assert json.loads(response.text) == {
106 'objects': expected_objects, 'transfer': 'basic'}
106 'objects': expected_objects, 'transfer': 'basic'}
107
107
108 def test_app_batch_api_download(self, git_lfs_app, http_auth):
108 def test_app_batch_api_download(self, git_lfs_app, http_auth):
109 oid = '456'
109 oid = '456'
110 oid_path = os.path.join(git_lfs_app._store, oid)
110 oid_path = os.path.join(git_lfs_app._store, oid)
111 if not os.path.isdir(os.path.dirname(oid_path)):
111 if not os.path.isdir(os.path.dirname(oid_path)):
112 os.makedirs(os.path.dirname(oid_path))
112 os.makedirs(os.path.dirname(oid_path))
113 with open(oid_path, 'wb') as f:
113 with open(oid_path, 'wb') as f:
114 f.write('OID_CONTENT')
114 f.write('OID_CONTENT')
115
115
116 params = {'operation': 'download',
116 params = {'operation': 'download',
117 'objects': [{'oid': oid, 'size': '1024'}]}
117 'objects': [{'oid': oid, 'size': '1024'}]}
118 response = git_lfs_app.post_json(
118 response = git_lfs_app.post_json(
119 '/repo/info/lfs/objects/batch', params=params,
119 '/repo/info/lfs/objects/batch', params=params,
120 extra_environ=http_auth)
120 extra_environ=http_auth)
121
121
122 expected_objects = [
122 expected_objects = [
123 {u'authenticated': True,
123 {u'authenticated': True,
124 u'actions': {
124 u'actions': {
125 u'download': {
125 u'download': {
126 u'header': {u'Authorization': u'Basic XXXXX'},
126 u'header': {u'Authorization': u'Basic XXXXX'},
127 u'href': u'http://localhost/repo/info/lfs/objects/456'},
127 u'href': u'http://localhost/repo/info/lfs/objects/456'},
128 },
128 },
129 u'oid': u'456',
129 u'oid': u'456',
130 u'size': u'1024'}
130 u'size': u'1024'}
131 ]
131 ]
132 assert json.loads(response.text) == {
132 assert json.loads(response.text) == {
133 'objects': expected_objects, 'transfer': 'basic'}
133 'objects': expected_objects, 'transfer': 'basic'}
134
134
135 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
135 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
136 params = {'operation': 'upload',
136 params = {'operation': 'upload',
137 'objects': [{'oid': '123', 'size': '1024'}]}
137 'objects': [{'oid': '123', 'size': '1024'}]}
138 response = git_lfs_app.post_json(
138 response = git_lfs_app.post_json(
139 '/repo/info/lfs/objects/batch', params=params,
139 '/repo/info/lfs/objects/batch', params=params,
140 extra_environ=http_auth)
140 extra_environ=http_auth)
141 expected_objects = [
141 expected_objects = [
142 {u'authenticated': True,
142 {u'authenticated': True,
143 u'actions': {
143 u'actions': {
144 u'upload': {
144 u'upload': {
145 u'header': {u'Authorization': u'Basic XXXXX',
145 u'header': {u'Authorization': u'Basic XXXXX',
146 u'Transfer-Encoding': u'chunked'},
146 u'Transfer-Encoding': u'chunked'},
147 u'href': u'http://localhost/repo/info/lfs/objects/123'},
147 u'href': u'http://localhost/repo/info/lfs/objects/123'},
148 u'verify': {
148 u'verify': {
149 u'header': {u'Authorization': u'Basic XXXXX'},
149 u'header': {u'Authorization': u'Basic XXXXX'},
150 u'href': u'http://localhost/repo/info/lfs/verify'}
150 u'href': u'http://localhost/repo/info/lfs/verify'}
151 },
151 },
152 u'oid': u'123',
152 u'oid': u'123',
153 u'size': u'1024'}
153 u'size': u'1024'}
154 ]
154 ]
155 assert json.loads(response.text) == {
155 assert json.loads(response.text) == {
156 'objects': expected_objects, 'transfer': 'basic'}
156 'objects': expected_objects, 'transfer': 'basic'}
157
157
158 def test_app_verify_api_missing_data(self, git_lfs_app):
158 def test_app_verify_api_missing_data(self, git_lfs_app):
159 params = {'oid': 'missing',}
159 params = {'oid': 'missing',}
160 response = git_lfs_app.post_json(
160 response = git_lfs_app.post_json(
161 '/repo/info/lfs/verify', params=params,
161 '/repo/info/lfs/verify', params=params,
162 status=400)
162 status=400)
163
163
164 assert json.loads(response.text) == {
164 assert json.loads(response.text) == {
165 u'message': u'missing oid and size in request data'}
165 u'message': u'missing oid and size in request data'}
166
166
167 def test_app_verify_api_missing_obj(self, git_lfs_app):
167 def test_app_verify_api_missing_obj(self, git_lfs_app):
168 params = {'oid': 'missing', 'size': '1024'}
168 params = {'oid': 'missing', 'size': '1024'}
169 response = git_lfs_app.post_json(
169 response = git_lfs_app.post_json(
170 '/repo/info/lfs/verify', params=params,
170 '/repo/info/lfs/verify', params=params,
171 status=404)
171 status=404)
172
172
173 assert json.loads(response.text) == {
173 assert json.loads(response.text) == {
174 u'message': u'oid `missing` does not exists in store'}
174 u'message': u'oid `missing` does not exists in store'}
175
175
176 def test_app_verify_api_size_mismatch(self, git_lfs_app):
176 def test_app_verify_api_size_mismatch(self, git_lfs_app):
177 oid = 'existing'
177 oid = 'existing'
178 oid_path = os.path.join(git_lfs_app._store, oid)
178 oid_path = os.path.join(git_lfs_app._store, oid)
179 if not os.path.isdir(os.path.dirname(oid_path)):
179 if not os.path.isdir(os.path.dirname(oid_path)):
180 os.makedirs(os.path.dirname(oid_path))
180 os.makedirs(os.path.dirname(oid_path))
181 with open(oid_path, 'wb') as f:
181 with open(oid_path, 'wb') as f:
182 f.write('OID_CONTENT')
182 f.write('OID_CONTENT')
183
183
184 params = {'oid': oid, 'size': '1024'}
184 params = {'oid': oid, 'size': '1024'}
185 response = git_lfs_app.post_json(
185 response = git_lfs_app.post_json(
186 '/repo/info/lfs/verify', params=params, status=422)
186 '/repo/info/lfs/verify', params=params, status=422)
187
187
188 assert json.loads(response.text) == {
188 assert json.loads(response.text) == {
189 u'message': u'requested file size mismatch '
189 u'message': u'requested file size mismatch '
190 u'store size:11 requested:1024'}
190 u'store size:11 requested:1024'}
191
191
192 def test_app_verify_api(self, git_lfs_app):
192 def test_app_verify_api(self, git_lfs_app):
193 oid = 'existing'
193 oid = 'existing'
194 oid_path = os.path.join(git_lfs_app._store, oid)
194 oid_path = os.path.join(git_lfs_app._store, oid)
195 if not os.path.isdir(os.path.dirname(oid_path)):
195 if not os.path.isdir(os.path.dirname(oid_path)):
196 os.makedirs(os.path.dirname(oid_path))
196 os.makedirs(os.path.dirname(oid_path))
197 with open(oid_path, 'wb') as f:
197 with open(oid_path, 'wb') as f:
198 f.write('OID_CONTENT')
198 f.write('OID_CONTENT')
199
199
200 params = {'oid': oid, 'size': 11}
200 params = {'oid': oid, 'size': 11}
201 response = git_lfs_app.post_json(
201 response = git_lfs_app.post_json(
202 '/repo/info/lfs/verify', params=params)
202 '/repo/info/lfs/verify', params=params)
203
203
204 assert json.loads(response.text) == {
204 assert json.loads(response.text) == {
205 u'message': {u'size': u'ok', u'in_store': u'ok'}}
205 u'message': {u'size': u'ok', u'in_store': u'ok'}}
206
206
207 def test_app_download_api_oid_not_existing(self, git_lfs_app):
207 def test_app_download_api_oid_not_existing(self, git_lfs_app):
208 oid = 'missing'
208 oid = 'missing'
209
209
210 response = git_lfs_app.get(
210 response = git_lfs_app.get(
211 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
211 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
212
212
213 assert json.loads(response.text) == {
213 assert json.loads(response.text) == {
214 u'message': u'requested file with oid `missing` not found in store'}
214 u'message': u'requested file with oid `missing` not found in store'}
215
215
216 def test_app_download_api(self, git_lfs_app):
216 def test_app_download_api(self, git_lfs_app):
217 oid = 'existing'
217 oid = 'existing'
218 oid_path = os.path.join(git_lfs_app._store, oid)
218 oid_path = os.path.join(git_lfs_app._store, oid)
219 if not os.path.isdir(os.path.dirname(oid_path)):
219 if not os.path.isdir(os.path.dirname(oid_path)):
220 os.makedirs(os.path.dirname(oid_path))
220 os.makedirs(os.path.dirname(oid_path))
221 with open(oid_path, 'wb') as f:
221 with open(oid_path, 'wb') as f:
222 f.write('OID_CONTENT')
222 f.write('OID_CONTENT')
223
223
224 response = git_lfs_app.get(
224 response = git_lfs_app.get(
225 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
225 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
226 assert response
226 assert response
227
227
228 def test_app_upload(self, git_lfs_app):
228 def test_app_upload(self, git_lfs_app):
229 oid = 'uploaded'
229 oid = 'uploaded'
230
230
231 response = git_lfs_app.put(
231 response = git_lfs_app.put(
232 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
232 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
233
233
234 assert json.loads(response.text) == {u'upload': u'ok'}
234 assert json.loads(response.text) == {u'upload': u'ok'}
235
235
236 # verify that we actually wrote that OID
236 # verify that we actually wrote that OID
237 oid_path = os.path.join(git_lfs_app._store, oid)
237 oid_path = os.path.join(git_lfs_app._store, oid)
238 assert os.path.isfile(oid_path)
238 assert os.path.isfile(oid_path)
239 assert 'CONTENT' == open(oid_path).read()
239 assert 'CONTENT' == open(oid_path).read()
@@ -1,141 +1,141 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import pytest
19 import pytest
20 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
20 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
21
21
22
22
23 @pytest.fixture()
23 @pytest.fixture()
24 def lfs_store(tmpdir):
24 def lfs_store(tmpdir):
25 repo = 'test'
25 repo = 'test'
26 oid = '123456789'
26 oid = '123456789'
27 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
27 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
28 return store
28 return store
29
29
30
30
31 @pytest.fixture()
31 @pytest.fixture()
32 def oid_handler(lfs_store):
32 def oid_handler(lfs_store):
33 store = lfs_store
33 store = lfs_store
34 repo = store.repo
34 repo = store.repo
35 oid = store.oid
35 oid = store.oid
36
36
37 oid_handler = OidHandler(
37 oid_handler = OidHandler(
38 store=store, repo_name=repo, auth=('basic', 'xxxx'),
38 store=store, repo_name=repo, auth=('basic', 'xxxx'),
39 oid=oid,
39 oid=oid,
40 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
40 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
41 obj_verify_href='http://localhost/verify')
41 obj_verify_href='http://localhost/verify')
42 return oid_handler
42 return oid_handler
43
43
44
44
45 class TestOidHandler(object):
45 class TestOidHandler(object):
46
46
47 @pytest.mark.parametrize('exec_action', [
47 @pytest.mark.parametrize('exec_action', [
48 'download',
48 'download',
49 'upload',
49 'upload',
50 ])
50 ])
51 def test_exec_action(self, exec_action, oid_handler):
51 def test_exec_action(self, exec_action, oid_handler):
52 handler = oid_handler.exec_operation(exec_action)
52 handler = oid_handler.exec_operation(exec_action)
53 assert handler
53 assert handler
54
54
55 def test_exec_action_undefined(self, oid_handler):
55 def test_exec_action_undefined(self, oid_handler):
56 with pytest.raises(AttributeError):
56 with pytest.raises(AttributeError):
57 oid_handler.exec_operation('wrong')
57 oid_handler.exec_operation('wrong')
58
58
59 def test_download_oid_not_existing(self, oid_handler):
59 def test_download_oid_not_existing(self, oid_handler):
60 response, has_errors = oid_handler.exec_operation('download')
60 response, has_errors = oid_handler.exec_operation('download')
61
61
62 assert response is None
62 assert response is None
63 assert has_errors['error'] == {
63 assert has_errors['error'] == {
64 'code': 404,
64 'code': 404,
65 'message': 'object: 123456789 does not exist in store'}
65 'message': 'object: 123456789 does not exist in store'}
66
66
67 def test_download_oid(self, oid_handler):
67 def test_download_oid(self, oid_handler):
68 store = oid_handler.get_store()
68 store = oid_handler.get_store()
69 if not os.path.isdir(os.path.dirname(store.oid_path)):
69 if not os.path.isdir(os.path.dirname(store.oid_path)):
70 os.makedirs(os.path.dirname(store.oid_path))
70 os.makedirs(os.path.dirname(store.oid_path))
71
71
72 with open(store.oid_path, 'wb') as f:
72 with open(store.oid_path, 'wb') as f:
73 f.write('CONTENT')
73 f.write('CONTENT')
74
74
75 response, has_errors = oid_handler.exec_operation('download')
75 response, has_errors = oid_handler.exec_operation('download')
76
76
77 assert has_errors is None
77 assert has_errors is None
78 assert response['download'] == {
78 assert response['download'] == {
79 'header': {'Authorization': 'basic xxxx'},
79 'header': {'Authorization': 'basic xxxx'},
80 'href': 'http://localhost/handle_oid'
80 'href': 'http://localhost/handle_oid'
81 }
81 }
82
82
83 def test_upload_oid_that_exists(self, oid_handler):
83 def test_upload_oid_that_exists(self, oid_handler):
84 store = oid_handler.get_store()
84 store = oid_handler.get_store()
85 if not os.path.isdir(os.path.dirname(store.oid_path)):
85 if not os.path.isdir(os.path.dirname(store.oid_path)):
86 os.makedirs(os.path.dirname(store.oid_path))
86 os.makedirs(os.path.dirname(store.oid_path))
87
87
88 with open(store.oid_path, 'wb') as f:
88 with open(store.oid_path, 'wb') as f:
89 f.write('CONTENT')
89 f.write('CONTENT')
90 oid_handler.obj_size = 7
90 oid_handler.obj_size = 7
91 response, has_errors = oid_handler.exec_operation('upload')
91 response, has_errors = oid_handler.exec_operation('upload')
92 assert has_errors is None
92 assert has_errors is None
93 assert response is None
93 assert response is None
94
94
95 def test_upload_oid_that_exists_but_has_wrong_size(self, oid_handler):
95 def test_upload_oid_that_exists_but_has_wrong_size(self, oid_handler):
96 store = oid_handler.get_store()
96 store = oid_handler.get_store()
97 if not os.path.isdir(os.path.dirname(store.oid_path)):
97 if not os.path.isdir(os.path.dirname(store.oid_path)):
98 os.makedirs(os.path.dirname(store.oid_path))
98 os.makedirs(os.path.dirname(store.oid_path))
99
99
100 with open(store.oid_path, 'wb') as f:
100 with open(store.oid_path, 'wb') as f:
101 f.write('CONTENT')
101 f.write('CONTENT')
102
102
103 oid_handler.obj_size = 10240
103 oid_handler.obj_size = 10240
104 response, has_errors = oid_handler.exec_operation('upload')
104 response, has_errors = oid_handler.exec_operation('upload')
105 assert has_errors is None
105 assert has_errors is None
106 assert response['upload'] == {
106 assert response['upload'] == {
107 'header': {'Authorization': 'basic xxxx',
107 'header': {'Authorization': 'basic xxxx',
108 'Transfer-Encoding': 'chunked'},
108 'Transfer-Encoding': 'chunked'},
109 'href': 'http://localhost/handle_oid',
109 'href': 'http://localhost/handle_oid',
110 }
110 }
111
111
112 def test_upload_oid(self, oid_handler):
112 def test_upload_oid(self, oid_handler):
113 response, has_errors = oid_handler.exec_operation('upload')
113 response, has_errors = oid_handler.exec_operation('upload')
114 assert has_errors is None
114 assert has_errors is None
115 assert response['upload'] == {
115 assert response['upload'] == {
116 'header': {'Authorization': 'basic xxxx',
116 'header': {'Authorization': 'basic xxxx',
117 'Transfer-Encoding': 'chunked'},
117 'Transfer-Encoding': 'chunked'},
118 'href': 'http://localhost/handle_oid'
118 'href': 'http://localhost/handle_oid'
119 }
119 }
120
120
121
121
122 class TestLFSStore(object):
122 class TestLFSStore(object):
123 def test_write_oid(self, lfs_store):
123 def test_write_oid(self, lfs_store):
124 oid_location = lfs_store.oid_path
124 oid_location = lfs_store.oid_path
125
125
126 assert not os.path.isfile(oid_location)
126 assert not os.path.isfile(oid_location)
127
127
128 engine = lfs_store.get_engine(mode='wb')
128 engine = lfs_store.get_engine(mode='wb')
129 with engine as f:
129 with engine as f:
130 f.write('CONTENT')
130 f.write('CONTENT')
131
131
132 assert os.path.isfile(oid_location)
132 assert os.path.isfile(oid_location)
133
133
134 def test_detect_has_oid(self, lfs_store):
134 def test_detect_has_oid(self, lfs_store):
135
135
136 assert lfs_store.has_oid() is False
136 assert lfs_store.has_oid() is False
137 engine = lfs_store.get_engine(mode='wb')
137 engine = lfs_store.get_engine(mode='wb')
138 with engine as f:
138 with engine as f:
139 f.write('CONTENT')
139 f.write('CONTENT')
140
140
141 assert lfs_store.has_oid() is True No newline at end of file
141 assert lfs_store.has_oid() is True
@@ -1,50 +1,50 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 import copy
17 import copy
18 from functools import wraps
18 from functools import wraps
19
19
20
20
21 def get_cython_compat_decorator(wrapper, func):
21 def get_cython_compat_decorator(wrapper, func):
22 """
22 """
23 Creates a cython compatible decorator. The previously used
23 Creates a cython compatible decorator. The previously used
24 decorator.decorator() function seems to be incompatible with cython.
24 decorator.decorator() function seems to be incompatible with cython.
25
25
26 :param wrapper: __wrapper method of the decorator class
26 :param wrapper: __wrapper method of the decorator class
27 :param func: decorated function
27 :param func: decorated function
28 """
28 """
29 @wraps(func)
29 @wraps(func)
30 def local_wrapper(*args, **kwds):
30 def local_wrapper(*args, **kwds):
31 return wrapper(func, *args, **kwds)
31 return wrapper(func, *args, **kwds)
32 local_wrapper.__wrapped__ = func
32 local_wrapper.__wrapped__ = func
33 return local_wrapper
33 return local_wrapper
34
34
35
35
36 def safe_result(result):
36 def safe_result(result):
37 """clean result for better representation in logs"""
37 """clean result for better representation in logs"""
38 clean_copy = copy.deepcopy(result)
38 clean_copy = copy.deepcopy(result)
39
39
40 try:
40 try:
41 if 'objects' in clean_copy:
41 if 'objects' in clean_copy:
42 for oid_data in clean_copy['objects']:
42 for oid_data in clean_copy['objects']:
43 if 'actions' in oid_data:
43 if 'actions' in oid_data:
44 for action_name, data in oid_data['actions'].items():
44 for action_name, data in oid_data['actions'].items():
45 if 'header' in data:
45 if 'header' in data:
46 data['header'] = {'Authorization': '*****'}
46 data['header'] = {'Authorization': '*****'}
47 except Exception:
47 except Exception:
48 return result
48 return result
49
49
50 return clean_copy
50 return clean_copy
@@ -1,795 +1,803 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import logging
19 import logging
20 import stat
20 import stat
21 import urllib
21 import urllib
22 import urllib2
22 import urllib2
23
23
24 from hgext import largefiles, rebase
24 from hgext import largefiles, rebase
25 from hgext.strip import strip as hgext_strip
25 from hgext.strip import strip as hgext_strip
26 from mercurial import commands
26 from mercurial import commands
27 from mercurial import unionrepo
27 from mercurial import unionrepo
28 from mercurial import verify
28 from mercurial import verify
29
29
30 import vcsserver
30 from vcsserver import exceptions
31 from vcsserver import exceptions
31 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
32 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
32 from vcsserver.hgcompat import (
33 from vcsserver.hgcompat import (
33 archival, bin, clone, config as hgconfig, diffopts, hex,
34 archival, bin, clone, config as hgconfig, diffopts, hex,
34 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
35 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
35 makepeer, localrepository, match, memctx, exchange, memfilectx, nullrev,
36 makepeer, localrepository, match, memctx, exchange, memfilectx, nullrev,
36 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
37 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
37 RepoLookupError, InterventionRequired, RequirementError)
38 RepoLookupError, InterventionRequired, RequirementError)
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 def make_ui_from_config(repo_config):
43 def make_ui_from_config(repo_config):
43 baseui = ui.ui()
44 baseui = ui.ui()
44
45
45 # clean the baseui object
46 # clean the baseui object
46 baseui._ocfg = hgconfig.config()
47 baseui._ocfg = hgconfig.config()
47 baseui._ucfg = hgconfig.config()
48 baseui._ucfg = hgconfig.config()
48 baseui._tcfg = hgconfig.config()
49 baseui._tcfg = hgconfig.config()
49
50
50 for section, option, value in repo_config:
51 for section, option, value in repo_config:
51 baseui.setconfig(section, option, value)
52 baseui.setconfig(section, option, value)
52
53
53 # make our hgweb quiet so it doesn't print output
54 # make our hgweb quiet so it doesn't print output
54 baseui.setconfig('ui', 'quiet', 'true')
55 baseui.setconfig('ui', 'quiet', 'true')
55
56
56 baseui.setconfig('ui', 'paginate', 'never')
57 baseui.setconfig('ui', 'paginate', 'never')
57 # force mercurial to only use 1 thread, otherwise it may try to set a
58 # force mercurial to only use 1 thread, otherwise it may try to set a
58 # signal in a non-main thread, thus generating a ValueError.
59 # signal in a non-main thread, thus generating a ValueError.
59 baseui.setconfig('worker', 'numcpus', 1)
60 baseui.setconfig('worker', 'numcpus', 1)
60
61
61 # If there is no config for the largefiles extension, we explicitly disable
62 # If there is no config for the largefiles extension, we explicitly disable
62 # it here. This overrides settings from repositories hgrc file. Recent
63 # it here. This overrides settings from repositories hgrc file. Recent
63 # mercurial versions enable largefiles in hgrc on clone from largefile
64 # mercurial versions enable largefiles in hgrc on clone from largefile
64 # repo.
65 # repo.
65 if not baseui.hasconfig('extensions', 'largefiles'):
66 if not baseui.hasconfig('extensions', 'largefiles'):
66 log.debug('Explicitly disable largefiles extension for repo.')
67 log.debug('Explicitly disable largefiles extension for repo.')
67 baseui.setconfig('extensions', 'largefiles', '!')
68 baseui.setconfig('extensions', 'largefiles', '!')
68
69
69 return baseui
70 return baseui
70
71
71
72
72 def reraise_safe_exceptions(func):
73 def reraise_safe_exceptions(func):
73 """Decorator for converting mercurial exceptions to something neutral."""
74 """Decorator for converting mercurial exceptions to something neutral."""
74 def wrapper(*args, **kwargs):
75 def wrapper(*args, **kwargs):
75 try:
76 try:
76 return func(*args, **kwargs)
77 return func(*args, **kwargs)
77 except (Abort, InterventionRequired) as e:
78 except (Abort, InterventionRequired) as e:
78 raise_from_original(exceptions.AbortException(e))
79 raise_from_original(exceptions.AbortException(e))
79 except RepoLookupError as e:
80 except RepoLookupError as e:
80 raise_from_original(exceptions.LookupException(e))
81 raise_from_original(exceptions.LookupException(e))
81 except RequirementError as e:
82 except RequirementError as e:
82 raise_from_original(exceptions.RequirementException(e))
83 raise_from_original(exceptions.RequirementException(e))
83 except RepoError as e:
84 except RepoError as e:
84 raise_from_original(exceptions.VcsException(e))
85 raise_from_original(exceptions.VcsException(e))
85 except LookupError as e:
86 except LookupError as e:
86 raise_from_original(exceptions.LookupException(e))
87 raise_from_original(exceptions.LookupException(e))
87 except Exception as e:
88 except Exception as e:
88 if not hasattr(e, '_vcs_kind'):
89 if not hasattr(e, '_vcs_kind'):
89 log.exception("Unhandled exception in hg remote call")
90 log.exception("Unhandled exception in hg remote call")
90 raise_from_original(exceptions.UnhandledException(e))
91 raise_from_original(exceptions.UnhandledException(e))
91
92
92 raise
93 raise
93 return wrapper
94 return wrapper
94
95
95
96
96 class MercurialFactory(RepoFactory):
97 class MercurialFactory(RepoFactory):
97 repo_type = 'hg'
98 repo_type = 'hg'
98
99
99 def _create_config(self, config, hooks=True):
100 def _create_config(self, config, hooks=True):
100 if not hooks:
101 if not hooks:
101 hooks_to_clean = frozenset((
102 hooks_to_clean = frozenset((
102 'changegroup.repo_size', 'preoutgoing.pre_pull',
103 'changegroup.repo_size', 'preoutgoing.pre_pull',
103 'outgoing.pull_logger', 'prechangegroup.pre_push'))
104 'outgoing.pull_logger', 'prechangegroup.pre_push'))
104 new_config = []
105 new_config = []
105 for section, option, value in config:
106 for section, option, value in config:
106 if section == 'hooks' and option in hooks_to_clean:
107 if section == 'hooks' and option in hooks_to_clean:
107 continue
108 continue
108 new_config.append((section, option, value))
109 new_config.append((section, option, value))
109 config = new_config
110 config = new_config
110
111
111 baseui = make_ui_from_config(config)
112 baseui = make_ui_from_config(config)
112 return baseui
113 return baseui
113
114
114 def _create_repo(self, wire, create):
115 def _create_repo(self, wire, create):
115 baseui = self._create_config(wire["config"])
116 baseui = self._create_config(wire["config"])
116 return localrepository(baseui, wire["path"], create)
117 return localrepository(baseui, wire["path"], create)
117
118
118
119
119 class HgRemote(object):
120 class HgRemote(object):
120
121
121 def __init__(self, factory):
122 def __init__(self, factory):
122 self._factory = factory
123 self._factory = factory
123
124
124 self._bulk_methods = {
125 self._bulk_methods = {
125 "affected_files": self.ctx_files,
126 "affected_files": self.ctx_files,
126 "author": self.ctx_user,
127 "author": self.ctx_user,
127 "branch": self.ctx_branch,
128 "branch": self.ctx_branch,
128 "children": self.ctx_children,
129 "children": self.ctx_children,
129 "date": self.ctx_date,
130 "date": self.ctx_date,
130 "message": self.ctx_description,
131 "message": self.ctx_description,
131 "parents": self.ctx_parents,
132 "parents": self.ctx_parents,
132 "status": self.ctx_status,
133 "status": self.ctx_status,
133 "obsolete": self.ctx_obsolete,
134 "obsolete": self.ctx_obsolete,
134 "phase": self.ctx_phase,
135 "phase": self.ctx_phase,
135 "hidden": self.ctx_hidden,
136 "hidden": self.ctx_hidden,
136 "_file_paths": self.ctx_list,
137 "_file_paths": self.ctx_list,
137 }
138 }
138
139
139 @reraise_safe_exceptions
140 @reraise_safe_exceptions
140 def discover_hg_version(self):
141 def discover_hg_version(self):
141 from mercurial import util
142 from mercurial import util
142 return util.version()
143 return util.version()
143
144
144 @reraise_safe_exceptions
145 @reraise_safe_exceptions
145 def archive_repo(self, archive_path, mtime, file_info, kind):
146 def archive_repo(self, archive_path, mtime, file_info, kind):
146 if kind == "tgz":
147 if kind == "tgz":
147 archiver = archival.tarit(archive_path, mtime, "gz")
148 archiver = archival.tarit(archive_path, mtime, "gz")
148 elif kind == "tbz2":
149 elif kind == "tbz2":
149 archiver = archival.tarit(archive_path, mtime, "bz2")
150 archiver = archival.tarit(archive_path, mtime, "bz2")
150 elif kind == 'zip':
151 elif kind == 'zip':
151 archiver = archival.zipit(archive_path, mtime)
152 archiver = archival.zipit(archive_path, mtime)
152 else:
153 else:
153 raise exceptions.ArchiveException()(
154 raise exceptions.ArchiveException()(
154 'Remote does not support: "%s".' % kind)
155 'Remote does not support: "%s".' % kind)
155
156
156 for f_path, f_mode, f_is_link, f_content in file_info:
157 for f_path, f_mode, f_is_link, f_content in file_info:
157 archiver.addfile(f_path, f_mode, f_is_link, f_content)
158 archiver.addfile(f_path, f_mode, f_is_link, f_content)
158 archiver.done()
159 archiver.done()
159
160
160 @reraise_safe_exceptions
161 @reraise_safe_exceptions
161 def bookmarks(self, wire):
162 def bookmarks(self, wire):
162 repo = self._factory.repo(wire)
163 repo = self._factory.repo(wire)
163 return dict(repo._bookmarks)
164 return dict(repo._bookmarks)
164
165
165 @reraise_safe_exceptions
166 @reraise_safe_exceptions
166 def branches(self, wire, normal, closed):
167 def branches(self, wire, normal, closed):
167 repo = self._factory.repo(wire)
168 repo = self._factory.repo(wire)
168 iter_branches = repo.branchmap().iterbranches()
169 iter_branches = repo.branchmap().iterbranches()
169 bt = {}
170 bt = {}
170 for branch_name, _heads, tip, is_closed in iter_branches:
171 for branch_name, _heads, tip, is_closed in iter_branches:
171 if normal and not is_closed:
172 if normal and not is_closed:
172 bt[branch_name] = tip
173 bt[branch_name] = tip
173 if closed and is_closed:
174 if closed and is_closed:
174 bt[branch_name] = tip
175 bt[branch_name] = tip
175
176
176 return bt
177 return bt
177
178
178 @reraise_safe_exceptions
179 @reraise_safe_exceptions
179 def bulk_request(self, wire, rev, pre_load):
180 def bulk_request(self, wire, rev, pre_load):
180 result = {}
181 result = {}
181 for attr in pre_load:
182 for attr in pre_load:
182 try:
183 try:
183 method = self._bulk_methods[attr]
184 method = self._bulk_methods[attr]
184 result[attr] = method(wire, rev)
185 result[attr] = method(wire, rev)
185 except KeyError as e:
186 except KeyError as e:
186 raise exceptions.VcsException(e)(
187 raise exceptions.VcsException(e)(
187 'Unknown bulk attribute: "%s"' % attr)
188 'Unknown bulk attribute: "%s"' % attr)
188 return result
189 return result
189
190
190 @reraise_safe_exceptions
191 @reraise_safe_exceptions
191 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
192 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
192 baseui = self._factory._create_config(wire["config"], hooks=hooks)
193 baseui = self._factory._create_config(wire["config"], hooks=hooks)
193 clone(baseui, source, dest, noupdate=not update_after_clone)
194 clone(baseui, source, dest, noupdate=not update_after_clone)
194
195
195 @reraise_safe_exceptions
196 @reraise_safe_exceptions
196 def commitctx(
197 def commitctx(
197 self, wire, message, parents, commit_time, commit_timezone,
198 self, wire, message, parents, commit_time, commit_timezone,
198 user, files, extra, removed, updated):
199 user, files, extra, removed, updated):
199
200
200 def _filectxfn(_repo, memctx, path):
201 def _filectxfn(_repo, memctx, path):
201 """
202 """
202 Marks given path as added/changed/removed in a given _repo. This is
203 Marks given path as added/changed/removed in a given _repo. This is
203 for internal mercurial commit function.
204 for internal mercurial commit function.
204 """
205 """
205
206
206 # check if this path is removed
207 # check if this path is removed
207 if path in removed:
208 if path in removed:
208 # returning None is a way to mark node for removal
209 # returning None is a way to mark node for removal
209 return None
210 return None
210
211
211 # check if this path is added
212 # check if this path is added
212 for node in updated:
213 for node in updated:
213 if node['path'] == path:
214 if node['path'] == path:
214 return memfilectx(
215 return memfilectx(
215 _repo,
216 _repo,
216 changectx=memctx,
217 changectx=memctx,
217 path=node['path'],
218 path=node['path'],
218 data=node['content'],
219 data=node['content'],
219 islink=False,
220 islink=False,
220 isexec=bool(node['mode'] & stat.S_IXUSR),
221 isexec=bool(node['mode'] & stat.S_IXUSR),
221 copied=False)
222 copied=False)
222
223
223 raise exceptions.AbortException()(
224 raise exceptions.AbortException()(
224 "Given path haven't been marked as added, "
225 "Given path haven't been marked as added, "
225 "changed or removed (%s)" % path)
226 "changed or removed (%s)" % path)
226
227
227 repo = self._factory.repo(wire)
228 repo = self._factory.repo(wire)
228
229
229 commit_ctx = memctx(
230 commit_ctx = memctx(
230 repo=repo,
231 repo=repo,
231 parents=parents,
232 parents=parents,
232 text=message,
233 text=message,
233 files=files,
234 files=files,
234 filectxfn=_filectxfn,
235 filectxfn=_filectxfn,
235 user=user,
236 user=user,
236 date=(commit_time, commit_timezone),
237 date=(commit_time, commit_timezone),
237 extra=extra)
238 extra=extra)
238
239
239 n = repo.commitctx(commit_ctx)
240 n = repo.commitctx(commit_ctx)
240 new_id = hex(n)
241 new_id = hex(n)
241
242
242 return new_id
243 return new_id
243
244
244 @reraise_safe_exceptions
245 @reraise_safe_exceptions
245 def ctx_branch(self, wire, revision):
246 def ctx_branch(self, wire, revision):
246 repo = self._factory.repo(wire)
247 repo = self._factory.repo(wire)
247 ctx = repo[revision]
248 ctx = repo[revision]
248 return ctx.branch()
249 return ctx.branch()
249
250
250 @reraise_safe_exceptions
251 @reraise_safe_exceptions
251 def ctx_children(self, wire, revision):
252 def ctx_children(self, wire, revision):
252 repo = self._factory.repo(wire)
253 repo = self._factory.repo(wire)
253 ctx = repo[revision]
254 ctx = repo[revision]
254 return [child.rev() for child in ctx.children()]
255 return [child.rev() for child in ctx.children()]
255
256
256 @reraise_safe_exceptions
257 @reraise_safe_exceptions
257 def ctx_date(self, wire, revision):
258 def ctx_date(self, wire, revision):
258 repo = self._factory.repo(wire)
259 repo = self._factory.repo(wire)
259 ctx = repo[revision]
260 ctx = repo[revision]
260 return ctx.date()
261 return ctx.date()
261
262
262 @reraise_safe_exceptions
263 @reraise_safe_exceptions
263 def ctx_description(self, wire, revision):
264 def ctx_description(self, wire, revision):
264 repo = self._factory.repo(wire)
265 repo = self._factory.repo(wire)
265 ctx = repo[revision]
266 ctx = repo[revision]
266 return ctx.description()
267 return ctx.description()
267
268
268 @reraise_safe_exceptions
269 @reraise_safe_exceptions
269 def ctx_diff(
270 def ctx_diff(
270 self, wire, revision, git=True, ignore_whitespace=True, context=3):
271 self, wire, revision, git=True, ignore_whitespace=True, context=3):
271 repo = self._factory.repo(wire)
272 repo = self._factory.repo(wire)
272 ctx = repo[revision]
273 ctx = repo[revision]
273 result = ctx.diff(
274 result = ctx.diff(
274 git=git, ignore_whitespace=ignore_whitespace, context=context)
275 git=git, ignore_whitespace=ignore_whitespace, context=context)
275 return list(result)
276 return list(result)
276
277
277 @reraise_safe_exceptions
278 @reraise_safe_exceptions
278 def ctx_files(self, wire, revision):
279 def ctx_files(self, wire, revision):
279 repo = self._factory.repo(wire)
280 repo = self._factory.repo(wire)
280 ctx = repo[revision]
281 ctx = repo[revision]
281 return ctx.files()
282 return ctx.files()
282
283
283 @reraise_safe_exceptions
284 @reraise_safe_exceptions
284 def ctx_list(self, path, revision):
285 def ctx_list(self, path, revision):
285 repo = self._factory.repo(path)
286 repo = self._factory.repo(path)
286 ctx = repo[revision]
287 ctx = repo[revision]
287 return list(ctx)
288 return list(ctx)
288
289
289 @reraise_safe_exceptions
290 @reraise_safe_exceptions
290 def ctx_parents(self, wire, revision):
291 def ctx_parents(self, wire, revision):
291 repo = self._factory.repo(wire)
292 repo = self._factory.repo(wire)
292 ctx = repo[revision]
293 ctx = repo[revision]
293 return [parent.rev() for parent in ctx.parents()]
294 return [parent.rev() for parent in ctx.parents()]
294
295
295 @reraise_safe_exceptions
296 @reraise_safe_exceptions
296 def ctx_phase(self, wire, revision):
297 def ctx_phase(self, wire, revision):
297 repo = self._factory.repo(wire)
298 repo = self._factory.repo(wire)
298 ctx = repo[revision]
299 ctx = repo[revision]
299 # public=0, draft=1, secret=3
300 # public=0, draft=1, secret=3
300 return ctx.phase()
301 return ctx.phase()
301
302
302 @reraise_safe_exceptions
303 @reraise_safe_exceptions
303 def ctx_obsolete(self, wire, revision):
304 def ctx_obsolete(self, wire, revision):
304 repo = self._factory.repo(wire)
305 repo = self._factory.repo(wire)
305 ctx = repo[revision]
306 ctx = repo[revision]
306 return ctx.obsolete()
307 return ctx.obsolete()
307
308
308 @reraise_safe_exceptions
309 @reraise_safe_exceptions
309 def ctx_hidden(self, wire, revision):
310 def ctx_hidden(self, wire, revision):
310 repo = self._factory.repo(wire)
311 repo = self._factory.repo(wire)
311 ctx = repo[revision]
312 ctx = repo[revision]
312 return ctx.hidden()
313 return ctx.hidden()
313
314
314 @reraise_safe_exceptions
315 @reraise_safe_exceptions
315 def ctx_substate(self, wire, revision):
316 def ctx_substate(self, wire, revision):
316 repo = self._factory.repo(wire)
317 repo = self._factory.repo(wire)
317 ctx = repo[revision]
318 ctx = repo[revision]
318 return ctx.substate
319 return ctx.substate
319
320
320 @reraise_safe_exceptions
321 @reraise_safe_exceptions
321 def ctx_status(self, wire, revision):
322 def ctx_status(self, wire, revision):
322 repo = self._factory.repo(wire)
323 repo = self._factory.repo(wire)
323 ctx = repo[revision]
324 ctx = repo[revision]
324 status = repo[ctx.p1().node()].status(other=ctx.node())
325 status = repo[ctx.p1().node()].status(other=ctx.node())
325 # object of status (odd, custom named tuple in mercurial) is not
326 # object of status (odd, custom named tuple in mercurial) is not
326 # correctly serializable, we make it a list, as the underling
327 # correctly serializable, we make it a list, as the underling
327 # API expects this to be a list
328 # API expects this to be a list
328 return list(status)
329 return list(status)
329
330
330 @reraise_safe_exceptions
331 @reraise_safe_exceptions
331 def ctx_user(self, wire, revision):
332 def ctx_user(self, wire, revision):
332 repo = self._factory.repo(wire)
333 repo = self._factory.repo(wire)
333 ctx = repo[revision]
334 ctx = repo[revision]
334 return ctx.user()
335 return ctx.user()
335
336
336 @reraise_safe_exceptions
337 @reraise_safe_exceptions
337 def check_url(self, url, config):
338 def check_url(self, url, config):
338 _proto = None
339 _proto = None
339 if '+' in url[:url.find('://')]:
340 if '+' in url[:url.find('://')]:
340 _proto = url[0:url.find('+')]
341 _proto = url[0:url.find('+')]
341 url = url[url.find('+') + 1:]
342 url = url[url.find('+') + 1:]
342 handlers = []
343 handlers = []
343 url_obj = url_parser(url)
344 url_obj = url_parser(url)
344 test_uri, authinfo = url_obj.authinfo()
345 test_uri, authinfo = url_obj.authinfo()
345 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
346 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
346 url_obj.query = obfuscate_qs(url_obj.query)
347 url_obj.query = obfuscate_qs(url_obj.query)
347
348
348 cleaned_uri = str(url_obj)
349 cleaned_uri = str(url_obj)
349 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
350 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
350
351
351 if authinfo:
352 if authinfo:
352 # create a password manager
353 # create a password manager
353 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
354 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
354 passmgr.add_password(*authinfo)
355 passmgr.add_password(*authinfo)
355
356
356 handlers.extend((httpbasicauthhandler(passmgr),
357 handlers.extend((httpbasicauthhandler(passmgr),
357 httpdigestauthhandler(passmgr)))
358 httpdigestauthhandler(passmgr)))
358
359
359 o = urllib2.build_opener(*handlers)
360 o = urllib2.build_opener(*handlers)
360 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
361 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
361 ('Accept', 'application/mercurial-0.1')]
362 ('Accept', 'application/mercurial-0.1')]
362
363
363 q = {"cmd": 'between'}
364 q = {"cmd": 'between'}
364 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
365 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
365 qs = '?%s' % urllib.urlencode(q)
366 qs = '?%s' % urllib.urlencode(q)
366 cu = "%s%s" % (test_uri, qs)
367 cu = "%s%s" % (test_uri, qs)
367 req = urllib2.Request(cu, None, {})
368 req = urllib2.Request(cu, None, {})
368
369
369 try:
370 try:
370 log.debug("Trying to open URL %s", cleaned_uri)
371 log.debug("Trying to open URL %s", cleaned_uri)
371 resp = o.open(req)
372 resp = o.open(req)
372 if resp.code != 200:
373 if resp.code != 200:
373 raise exceptions.URLError()('Return Code is not 200')
374 raise exceptions.URLError()('Return Code is not 200')
374 except Exception as e:
375 except Exception as e:
375 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
376 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
376 # means it cannot be cloned
377 # means it cannot be cloned
377 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
378 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
378
379
379 # now check if it's a proper hg repo, but don't do it for svn
380 # now check if it's a proper hg repo, but don't do it for svn
380 try:
381 try:
381 if _proto == 'svn':
382 if _proto == 'svn':
382 pass
383 pass
383 else:
384 else:
384 # check for pure hg repos
385 # check for pure hg repos
385 log.debug(
386 log.debug(
386 "Verifying if URL is a Mercurial repository: %s",
387 "Verifying if URL is a Mercurial repository: %s",
387 cleaned_uri)
388 cleaned_uri)
388 ui = make_ui_from_config(config)
389 ui = make_ui_from_config(config)
389 peer_checker = makepeer(ui, url)
390 peer_checker = makepeer(ui, url)
390 peer_checker.lookup('tip')
391 peer_checker.lookup('tip')
391 except Exception as e:
392 except Exception as e:
392 log.warning("URL is not a valid Mercurial repository: %s",
393 log.warning("URL is not a valid Mercurial repository: %s",
393 cleaned_uri)
394 cleaned_uri)
394 raise exceptions.URLError(e)(
395 raise exceptions.URLError(e)(
395 "url [%s] does not look like an hg repo org_exc: %s"
396 "url [%s] does not look like an hg repo org_exc: %s"
396 % (cleaned_uri, e))
397 % (cleaned_uri, e))
397
398
398 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
399 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
399 return True
400 return True
400
401
401 @reraise_safe_exceptions
402 @reraise_safe_exceptions
402 def diff(
403 def diff(
403 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
404 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
404 context):
405 context):
405 repo = self._factory.repo(wire)
406 repo = self._factory.repo(wire)
406
407
407 if file_filter:
408 if file_filter:
408 match_filter = match(file_filter[0], '', [file_filter[1]])
409 match_filter = match(file_filter[0], '', [file_filter[1]])
409 else:
410 else:
410 match_filter = file_filter
411 match_filter = file_filter
411 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
412 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
412
413
413 try:
414 try:
414 return "".join(patch.diff(
415 return "".join(patch.diff(
415 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
416 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
416 except RepoLookupError as e:
417 except RepoLookupError as e:
417 raise exceptions.LookupException(e)()
418 raise exceptions.LookupException(e)()
418
419
419 @reraise_safe_exceptions
420 @reraise_safe_exceptions
420 def node_history(self, wire, revision, path, limit):
421 def node_history(self, wire, revision, path, limit):
421 repo = self._factory.repo(wire)
422 repo = self._factory.repo(wire)
422
423
423 ctx = repo[revision]
424 ctx = repo[revision]
424 fctx = ctx.filectx(path)
425 fctx = ctx.filectx(path)
425
426
426 def history_iter():
427 def history_iter():
427 limit_rev = fctx.rev()
428 limit_rev = fctx.rev()
428 for obj in reversed(list(fctx.filelog())):
429 for obj in reversed(list(fctx.filelog())):
429 obj = fctx.filectx(obj)
430 obj = fctx.filectx(obj)
430 if limit_rev >= obj.rev():
431 if limit_rev >= obj.rev():
431 yield obj
432 yield obj
432
433
433 history = []
434 history = []
434 for cnt, obj in enumerate(history_iter()):
435 for cnt, obj in enumerate(history_iter()):
435 if limit and cnt >= limit:
436 if limit and cnt >= limit:
436 break
437 break
437 history.append(hex(obj.node()))
438 history.append(hex(obj.node()))
438
439
439 return [x for x in history]
440 return [x for x in history]
440
441
441 @reraise_safe_exceptions
442 @reraise_safe_exceptions
442 def node_history_untill(self, wire, revision, path, limit):
443 def node_history_untill(self, wire, revision, path, limit):
443 repo = self._factory.repo(wire)
444 repo = self._factory.repo(wire)
444 ctx = repo[revision]
445 ctx = repo[revision]
445 fctx = ctx.filectx(path)
446 fctx = ctx.filectx(path)
446
447
447 file_log = list(fctx.filelog())
448 file_log = list(fctx.filelog())
448 if limit:
449 if limit:
449 # Limit to the last n items
450 # Limit to the last n items
450 file_log = file_log[-limit:]
451 file_log = file_log[-limit:]
451
452
452 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
453 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
453
454
454 @reraise_safe_exceptions
455 @reraise_safe_exceptions
455 def fctx_annotate(self, wire, revision, path):
456 def fctx_annotate(self, wire, revision, path):
456 repo = self._factory.repo(wire)
457 repo = self._factory.repo(wire)
457 ctx = repo[revision]
458 ctx = repo[revision]
458 fctx = ctx.filectx(path)
459 fctx = ctx.filectx(path)
459
460
460 result = []
461 result = []
461 for i, annotate_obj in enumerate(fctx.annotate(), 1):
462 for i, annotate_obj in enumerate(fctx.annotate(), 1):
462 ln_no = i
463 ln_no = i
463 sha = hex(annotate_obj.fctx.node())
464 sha = hex(annotate_obj.fctx.node())
464 content = annotate_obj.text
465 content = annotate_obj.text
465 result.append((ln_no, sha, content))
466 result.append((ln_no, sha, content))
466 return result
467 return result
467
468
468 @reraise_safe_exceptions
469 @reraise_safe_exceptions
469 def fctx_data(self, wire, revision, path):
470 def fctx_data(self, wire, revision, path):
470 repo = self._factory.repo(wire)
471 repo = self._factory.repo(wire)
471 ctx = repo[revision]
472 ctx = repo[revision]
472 fctx = ctx.filectx(path)
473 fctx = ctx.filectx(path)
473 return fctx.data()
474 return fctx.data()
474
475
475 @reraise_safe_exceptions
476 @reraise_safe_exceptions
476 def fctx_flags(self, wire, revision, path):
477 def fctx_flags(self, wire, revision, path):
477 repo = self._factory.repo(wire)
478 repo = self._factory.repo(wire)
478 ctx = repo[revision]
479 ctx = repo[revision]
479 fctx = ctx.filectx(path)
480 fctx = ctx.filectx(path)
480 return fctx.flags()
481 return fctx.flags()
481
482
482 @reraise_safe_exceptions
483 @reraise_safe_exceptions
483 def fctx_size(self, wire, revision, path):
484 def fctx_size(self, wire, revision, path):
484 repo = self._factory.repo(wire)
485 repo = self._factory.repo(wire)
485 ctx = repo[revision]
486 ctx = repo[revision]
486 fctx = ctx.filectx(path)
487 fctx = ctx.filectx(path)
487 return fctx.size()
488 return fctx.size()
488
489
489 @reraise_safe_exceptions
490 @reraise_safe_exceptions
490 def get_all_commit_ids(self, wire, name):
491 def get_all_commit_ids(self, wire, name):
491 repo = self._factory.repo(wire)
492 repo = self._factory.repo(wire)
492 revs = repo.filtered(name).changelog.index
493 revs = repo.filtered(name).changelog.index
493 return map(lambda x: hex(x[7]), revs)[:-1]
494 return map(lambda x: hex(x[7]), revs)[:-1]
494
495
495 @reraise_safe_exceptions
496 @reraise_safe_exceptions
496 def get_config_value(self, wire, section, name, untrusted=False):
497 def get_config_value(self, wire, section, name, untrusted=False):
497 repo = self._factory.repo(wire)
498 repo = self._factory.repo(wire)
498 return repo.ui.config(section, name, untrusted=untrusted)
499 return repo.ui.config(section, name, untrusted=untrusted)
499
500
500 @reraise_safe_exceptions
501 @reraise_safe_exceptions
501 def get_config_bool(self, wire, section, name, untrusted=False):
502 def get_config_bool(self, wire, section, name, untrusted=False):
502 repo = self._factory.repo(wire)
503 repo = self._factory.repo(wire)
503 return repo.ui.configbool(section, name, untrusted=untrusted)
504 return repo.ui.configbool(section, name, untrusted=untrusted)
504
505
505 @reraise_safe_exceptions
506 @reraise_safe_exceptions
506 def get_config_list(self, wire, section, name, untrusted=False):
507 def get_config_list(self, wire, section, name, untrusted=False):
507 repo = self._factory.repo(wire)
508 repo = self._factory.repo(wire)
508 return repo.ui.configlist(section, name, untrusted=untrusted)
509 return repo.ui.configlist(section, name, untrusted=untrusted)
509
510
510 @reraise_safe_exceptions
511 @reraise_safe_exceptions
511 def is_large_file(self, wire, path):
512 def is_large_file(self, wire, path):
512 return largefiles.lfutil.isstandin(path)
513 return largefiles.lfutil.isstandin(path)
513
514
514 @reraise_safe_exceptions
515 @reraise_safe_exceptions
515 def in_largefiles_store(self, wire, sha):
516 def in_largefiles_store(self, wire, sha):
516 repo = self._factory.repo(wire)
517 repo = self._factory.repo(wire)
517 return largefiles.lfutil.instore(repo, sha)
518 return largefiles.lfutil.instore(repo, sha)
518
519
519 @reraise_safe_exceptions
520 @reraise_safe_exceptions
520 def in_user_cache(self, wire, sha):
521 def in_user_cache(self, wire, sha):
521 repo = self._factory.repo(wire)
522 repo = self._factory.repo(wire)
522 return largefiles.lfutil.inusercache(repo.ui, sha)
523 return largefiles.lfutil.inusercache(repo.ui, sha)
523
524
524 @reraise_safe_exceptions
525 @reraise_safe_exceptions
525 def store_path(self, wire, sha):
526 def store_path(self, wire, sha):
526 repo = self._factory.repo(wire)
527 repo = self._factory.repo(wire)
527 return largefiles.lfutil.storepath(repo, sha)
528 return largefiles.lfutil.storepath(repo, sha)
528
529
529 @reraise_safe_exceptions
530 @reraise_safe_exceptions
530 def link(self, wire, sha, path):
531 def link(self, wire, sha, path):
531 repo = self._factory.repo(wire)
532 repo = self._factory.repo(wire)
532 largefiles.lfutil.link(
533 largefiles.lfutil.link(
533 largefiles.lfutil.usercachepath(repo.ui, sha), path)
534 largefiles.lfutil.usercachepath(repo.ui, sha), path)
534
535
535 @reraise_safe_exceptions
536 @reraise_safe_exceptions
536 def localrepository(self, wire, create=False):
537 def localrepository(self, wire, create=False):
537 self._factory.repo(wire, create=create)
538 self._factory.repo(wire, create=create)
538
539
539 @reraise_safe_exceptions
540 @reraise_safe_exceptions
540 def lookup(self, wire, revision, both):
541 def lookup(self, wire, revision, both):
541
542
542 repo = self._factory.repo(wire)
543 repo = self._factory.repo(wire)
543
544
544 if isinstance(revision, int):
545 if isinstance(revision, int):
545 # NOTE(marcink):
546 # NOTE(marcink):
546 # since Mercurial doesn't support indexes properly
547 # since Mercurial doesn't support indexes properly
547 # we need to shift accordingly by one to get proper index, e.g
548 # we need to shift accordingly by one to get proper index, e.g
548 # repo[-1] => repo[-2]
549 # repo[-1] => repo[-2]
549 # repo[0] => repo[-1]
550 # repo[0] => repo[-1]
550 # repo[1] => repo[2] we also never call repo[0] because
551 # repo[1] => repo[2] we also never call repo[0] because
551 # it's actually second commit
552 # it's actually second commit
552 if revision <= 0:
553 if revision <= 0:
553 revision = revision + -1
554 revision = revision + -1
554 else:
555 else:
555 revision = revision + 1
556 revision = revision + 1
556
557
557 try:
558 try:
558 ctx = repo[revision]
559 ctx = repo[revision]
559 except RepoLookupError as e:
560 except RepoLookupError as e:
560 raise exceptions.LookupException(e)(revision)
561 raise exceptions.LookupException(e)(revision)
561 except LookupError as e:
562 except LookupError as e:
562 raise exceptions.LookupException(e)(e.name)
563 raise exceptions.LookupException(e)(e.name)
563
564
564 if not both:
565 if not both:
565 return ctx.hex()
566 return ctx.hex()
566
567
567 ctx = repo[ctx.hex()]
568 ctx = repo[ctx.hex()]
568 return ctx.hex(), ctx.rev()
569 return ctx.hex(), ctx.rev()
569
570
570 @reraise_safe_exceptions
571 @reraise_safe_exceptions
571 def pull(self, wire, url, commit_ids=None):
572 def pull(self, wire, url, commit_ids=None):
572 repo = self._factory.repo(wire)
573 repo = self._factory.repo(wire)
573 # Disable any prompts for this repo
574 # Disable any prompts for this repo
574 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
575 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
575
576
576 remote = peer(repo, {}, url)
577 remote = peer(repo, {}, url)
577 # Disable any prompts for this remote
578 # Disable any prompts for this remote
578 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
579 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
579
580
580 if commit_ids:
581 if commit_ids:
581 commit_ids = [bin(commit_id) for commit_id in commit_ids]
582 commit_ids = [bin(commit_id) for commit_id in commit_ids]
582
583
583 return exchange.pull(
584 return exchange.pull(
584 repo, remote, heads=commit_ids, force=None).cgresult
585 repo, remote, heads=commit_ids, force=None).cgresult
585
586
586 @reraise_safe_exceptions
587 @reraise_safe_exceptions
587 def sync_push(self, wire, url):
588 def sync_push(self, wire, url):
588 if not self.check_url(url, wire['config']):
589 if not self.check_url(url, wire['config']):
589 return
590 return
590
591
591 repo = self._factory.repo(wire)
592 repo = self._factory.repo(wire)
592
593
593 # Disable any prompts for this repo
594 # Disable any prompts for this repo
594 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
595 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
595
596
596 bookmarks = dict(repo._bookmarks).keys()
597 bookmarks = dict(repo._bookmarks).keys()
597 remote = peer(repo, {}, url)
598 remote = peer(repo, {}, url)
598 # Disable any prompts for this remote
599 # Disable any prompts for this remote
599 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
600 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
600
601
601 return exchange.push(
602 return exchange.push(
602 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
603 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
603
604
604 @reraise_safe_exceptions
605 @reraise_safe_exceptions
605 def revision(self, wire, rev):
606 def revision(self, wire, rev):
606 repo = self._factory.repo(wire)
607 repo = self._factory.repo(wire)
607 ctx = repo[rev]
608 ctx = repo[rev]
608 return ctx.rev()
609 return ctx.rev()
609
610
610 @reraise_safe_exceptions
611 @reraise_safe_exceptions
611 def rev_range(self, wire, filter):
612 def rev_range(self, wire, filter):
612 repo = self._factory.repo(wire)
613 repo = self._factory.repo(wire)
613 revisions = [rev for rev in revrange(repo, filter)]
614 revisions = [rev for rev in revrange(repo, filter)]
614 return revisions
615 return revisions
615
616
616 @reraise_safe_exceptions
617 @reraise_safe_exceptions
617 def rev_range_hash(self, wire, node):
618 def rev_range_hash(self, wire, node):
618 repo = self._factory.repo(wire)
619 repo = self._factory.repo(wire)
619
620
620 def get_revs(repo, rev_opt):
621 def get_revs(repo, rev_opt):
621 if rev_opt:
622 if rev_opt:
622 revs = revrange(repo, rev_opt)
623 revs = revrange(repo, rev_opt)
623 if len(revs) == 0:
624 if len(revs) == 0:
624 return (nullrev, nullrev)
625 return (nullrev, nullrev)
625 return max(revs), min(revs)
626 return max(revs), min(revs)
626 else:
627 else:
627 return len(repo) - 1, 0
628 return len(repo) - 1, 0
628
629
629 stop, start = get_revs(repo, [node + ':'])
630 stop, start = get_revs(repo, [node + ':'])
630 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
631 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
631 return revs
632 return revs
632
633
633 @reraise_safe_exceptions
634 @reraise_safe_exceptions
634 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
635 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
635 other_path = kwargs.pop('other_path', None)
636 other_path = kwargs.pop('other_path', None)
636
637
637 # case when we want to compare two independent repositories
638 # case when we want to compare two independent repositories
638 if other_path and other_path != wire["path"]:
639 if other_path and other_path != wire["path"]:
639 baseui = self._factory._create_config(wire["config"])
640 baseui = self._factory._create_config(wire["config"])
640 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
641 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
641 else:
642 else:
642 repo = self._factory.repo(wire)
643 repo = self._factory.repo(wire)
643 return list(repo.revs(rev_spec, *args))
644 return list(repo.revs(rev_spec, *args))
644
645
645 @reraise_safe_exceptions
646 @reraise_safe_exceptions
646 def strip(self, wire, revision, update, backup):
647 def strip(self, wire, revision, update, backup):
647 repo = self._factory.repo(wire)
648 repo = self._factory.repo(wire)
648 ctx = repo[revision]
649 ctx = repo[revision]
649 hgext_strip(
650 hgext_strip(
650 repo.baseui, repo, ctx.node(), update=update, backup=backup)
651 repo.baseui, repo, ctx.node(), update=update, backup=backup)
651
652
652 @reraise_safe_exceptions
653 @reraise_safe_exceptions
653 def verify(self, wire,):
654 def verify(self, wire,):
654 repo = self._factory.repo(wire)
655 repo = self._factory.repo(wire)
655 baseui = self._factory._create_config(wire['config'])
656 baseui = self._factory._create_config(wire['config'])
656 baseui.setconfig('ui', 'quiet', 'false')
657 baseui.setconfig('ui', 'quiet', 'false')
657 output = io.BytesIO()
658 output = io.BytesIO()
658
659
659 def write(data, **unused_kwargs):
660 def write(data, **unused_kwargs):
660 output.write(data)
661 output.write(data)
661 baseui.write = write
662 baseui.write = write
662
663
663 repo.ui = baseui
664 repo.ui = baseui
664 verify.verify(repo)
665 verify.verify(repo)
665 return output.getvalue()
666 return output.getvalue()
666
667
667 @reraise_safe_exceptions
668 @reraise_safe_exceptions
668 def tag(self, wire, name, revision, message, local, user,
669 def tag(self, wire, name, revision, message, local, user,
669 tag_time, tag_timezone):
670 tag_time, tag_timezone):
670 repo = self._factory.repo(wire)
671 repo = self._factory.repo(wire)
671 ctx = repo[revision]
672 ctx = repo[revision]
672 node = ctx.node()
673 node = ctx.node()
673
674
674 date = (tag_time, tag_timezone)
675 date = (tag_time, tag_timezone)
675 try:
676 try:
676 hg_tag.tag(repo, name, node, message, local, user, date)
677 hg_tag.tag(repo, name, node, message, local, user, date)
677 except Abort as e:
678 except Abort as e:
678 log.exception("Tag operation aborted")
679 log.exception("Tag operation aborted")
679 # Exception can contain unicode which we convert
680 # Exception can contain unicode which we convert
680 raise exceptions.AbortException(e)(repr(e))
681 raise exceptions.AbortException(e)(repr(e))
681
682
682 @reraise_safe_exceptions
683 @reraise_safe_exceptions
683 def tags(self, wire):
684 def tags(self, wire):
684 repo = self._factory.repo(wire)
685 repo = self._factory.repo(wire)
685 return repo.tags()
686 return repo.tags()
686
687
687 @reraise_safe_exceptions
688 @reraise_safe_exceptions
688 def update(self, wire, node=None, clean=False):
689 def update(self, wire, node=None, clean=False):
689 repo = self._factory.repo(wire)
690 repo = self._factory.repo(wire)
690 baseui = self._factory._create_config(wire['config'])
691 baseui = self._factory._create_config(wire['config'])
691 commands.update(baseui, repo, node=node, clean=clean)
692 commands.update(baseui, repo, node=node, clean=clean)
692
693
693 @reraise_safe_exceptions
694 @reraise_safe_exceptions
694 def identify(self, wire):
695 def identify(self, wire):
695 repo = self._factory.repo(wire)
696 repo = self._factory.repo(wire)
696 baseui = self._factory._create_config(wire['config'])
697 baseui = self._factory._create_config(wire['config'])
697 output = io.BytesIO()
698 output = io.BytesIO()
698 baseui.write = output.write
699 baseui.write = output.write
699 # This is required to get a full node id
700 # This is required to get a full node id
700 baseui.debugflag = True
701 baseui.debugflag = True
701 commands.identify(baseui, repo, id=True)
702 commands.identify(baseui, repo, id=True)
702
703
703 return output.getvalue()
704 return output.getvalue()
704
705
705 @reraise_safe_exceptions
706 @reraise_safe_exceptions
706 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
707 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
707 hooks=True):
708 hooks=True):
708 repo = self._factory.repo(wire)
709 repo = self._factory.repo(wire)
709 baseui = self._factory._create_config(wire['config'], hooks=hooks)
710 baseui = self._factory._create_config(wire['config'], hooks=hooks)
710
711
711 # Mercurial internally has a lot of logic that checks ONLY if
712 # Mercurial internally has a lot of logic that checks ONLY if
712 # option is defined, we just pass those if they are defined then
713 # option is defined, we just pass those if they are defined then
713 opts = {}
714 opts = {}
714 if bookmark:
715 if bookmark:
715 opts['bookmark'] = bookmark
716 opts['bookmark'] = bookmark
716 if branch:
717 if branch:
717 opts['branch'] = branch
718 opts['branch'] = branch
718 if revision:
719 if revision:
719 opts['rev'] = revision
720 opts['rev'] = revision
720
721
721 commands.pull(baseui, repo, source, **opts)
722 commands.pull(baseui, repo, source, **opts)
722
723
723 @reraise_safe_exceptions
724 @reraise_safe_exceptions
724 def heads(self, wire, branch=None):
725 def heads(self, wire, branch=None):
725 repo = self._factory.repo(wire)
726 repo = self._factory.repo(wire)
726 baseui = self._factory._create_config(wire['config'])
727 baseui = self._factory._create_config(wire['config'])
727 output = io.BytesIO()
728 output = io.BytesIO()
728
729
729 def write(data, **unused_kwargs):
730 def write(data, **unused_kwargs):
730 output.write(data)
731 output.write(data)
731
732
732 baseui.write = write
733 baseui.write = write
733 if branch:
734 if branch:
734 args = [branch]
735 args = [branch]
735 else:
736 else:
736 args = []
737 args = []
737 commands.heads(baseui, repo, template='{node} ', *args)
738 commands.heads(baseui, repo, template='{node} ', *args)
738
739
739 return output.getvalue()
740 return output.getvalue()
740
741
741 @reraise_safe_exceptions
742 @reraise_safe_exceptions
742 def ancestor(self, wire, revision1, revision2):
743 def ancestor(self, wire, revision1, revision2):
743 repo = self._factory.repo(wire)
744 repo = self._factory.repo(wire)
744 changelog = repo.changelog
745 changelog = repo.changelog
745 lookup = repo.lookup
746 lookup = repo.lookup
746 a = changelog.ancestor(lookup(revision1), lookup(revision2))
747 a = changelog.ancestor(lookup(revision1), lookup(revision2))
747 return hex(a)
748 return hex(a)
748
749
749 @reraise_safe_exceptions
750 @reraise_safe_exceptions
750 def push(self, wire, revisions, dest_path, hooks=True,
751 def push(self, wire, revisions, dest_path, hooks=True,
751 push_branches=False):
752 push_branches=False):
752 repo = self._factory.repo(wire)
753 repo = self._factory.repo(wire)
753 baseui = self._factory._create_config(wire['config'], hooks=hooks)
754 baseui = self._factory._create_config(wire['config'], hooks=hooks)
754 commands.push(baseui, repo, dest=dest_path, rev=revisions,
755 commands.push(baseui, repo, dest=dest_path, rev=revisions,
755 new_branch=push_branches)
756 new_branch=push_branches)
756
757
757 @reraise_safe_exceptions
758 @reraise_safe_exceptions
758 def merge(self, wire, revision):
759 def merge(self, wire, revision):
759 repo = self._factory.repo(wire)
760 repo = self._factory.repo(wire)
760 baseui = self._factory._create_config(wire['config'])
761 baseui = self._factory._create_config(wire['config'])
761 repo.ui.setconfig('ui', 'merge', 'internal:dump')
762 repo.ui.setconfig('ui', 'merge', 'internal:dump')
762
763
763 # In case of sub repositories are used mercurial prompts the user in
764 # In case of sub repositories are used mercurial prompts the user in
764 # case of merge conflicts or different sub repository sources. By
765 # case of merge conflicts or different sub repository sources. By
765 # setting the interactive flag to `False` mercurial doesn't prompt the
766 # setting the interactive flag to `False` mercurial doesn't prompt the
766 # used but instead uses a default value.
767 # used but instead uses a default value.
767 repo.ui.setconfig('ui', 'interactive', False)
768 repo.ui.setconfig('ui', 'interactive', False)
768
769
769 commands.merge(baseui, repo, rev=revision)
770 commands.merge(baseui, repo, rev=revision)
770
771
771 @reraise_safe_exceptions
772 @reraise_safe_exceptions
772 def commit(self, wire, message, username, close_branch=False):
773 def commit(self, wire, message, username, close_branch=False):
773 repo = self._factory.repo(wire)
774 repo = self._factory.repo(wire)
774 baseui = self._factory._create_config(wire['config'])
775 baseui = self._factory._create_config(wire['config'])
775 repo.ui.setconfig('ui', 'username', username)
776 repo.ui.setconfig('ui', 'username', username)
776 commands.commit(baseui, repo, message=message, close_branch=close_branch)
777 commands.commit(baseui, repo, message=message, close_branch=close_branch)
777
778
778 @reraise_safe_exceptions
779 @reraise_safe_exceptions
779 def rebase(self, wire, source=None, dest=None, abort=False):
780 def rebase(self, wire, source=None, dest=None, abort=False):
780 repo = self._factory.repo(wire)
781 repo = self._factory.repo(wire)
781 baseui = self._factory._create_config(wire['config'])
782 baseui = self._factory._create_config(wire['config'])
782 repo.ui.setconfig('ui', 'merge', 'internal:dump')
783 repo.ui.setconfig('ui', 'merge', 'internal:dump')
783 rebase.rebase(
784 rebase.rebase(
784 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
785 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
785
786
786 @reraise_safe_exceptions
787 @reraise_safe_exceptions
787 def bookmark(self, wire, bookmark, revision=None):
788 def bookmark(self, wire, bookmark, revision=None):
788 repo = self._factory.repo(wire)
789 repo = self._factory.repo(wire)
789 baseui = self._factory._create_config(wire['config'])
790 baseui = self._factory._create_config(wire['config'])
790 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
791 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
791
792
792 @reraise_safe_exceptions
793 @reraise_safe_exceptions
793 def install_hooks(self, wire, force=False):
794 def install_hooks(self, wire, force=False):
794 # we don't need any special hooks for Mercurial
795 # we don't need any special hooks for Mercurial
795 pass
796 pass
797
798 @reraise_safe_exceptions
799 def get_hooks_info(self, wire):
800 return {
801 'pre_version': vcsserver.__version__,
802 'post_version': vcsserver.__version__,
803 }
@@ -1,63 +1,63 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Mercurial libs compatibility
19 Mercurial libs compatibility
20 """
20 """
21
21
22 import mercurial
22 import mercurial
23 from mercurial import demandimport
23 from mercurial import demandimport
24 # patch demandimport, due to bug in mercurial when it always triggers
24 # patch demandimport, due to bug in mercurial when it always triggers
25 # demandimport.enable()
25 # demandimport.enable()
26 demandimport.enable = lambda *args, **kwargs: 1
26 demandimport.enable = lambda *args, **kwargs: 1
27
27
28 from mercurial import ui
28 from mercurial import ui
29 from mercurial import patch
29 from mercurial import patch
30 from mercurial import config
30 from mercurial import config
31 from mercurial import extensions
31 from mercurial import extensions
32 from mercurial import scmutil
32 from mercurial import scmutil
33 from mercurial import archival
33 from mercurial import archival
34 from mercurial import discovery
34 from mercurial import discovery
35 from mercurial import unionrepo
35 from mercurial import unionrepo
36 from mercurial import localrepo
36 from mercurial import localrepo
37 from mercurial import merge as hg_merge
37 from mercurial import merge as hg_merge
38 from mercurial import subrepo
38 from mercurial import subrepo
39 from mercurial import tags as hg_tag
39 from mercurial import tags as hg_tag
40
40
41 from mercurial.commands import clone, nullid, pull
41 from mercurial.commands import clone, nullid, pull
42 from mercurial.context import memctx, memfilectx
42 from mercurial.context import memctx, memfilectx
43 from mercurial.error import (
43 from mercurial.error import (
44 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
44 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
45 RequirementError)
45 RequirementError)
46 from mercurial.hgweb import hgweb_mod
46 from mercurial.hgweb import hgweb_mod
47 from mercurial.localrepo import localrepository
47 from mercurial.localrepo import localrepository
48 from mercurial.match import match
48 from mercurial.match import match
49 from mercurial.mdiff import diffopts
49 from mercurial.mdiff import diffopts
50 from mercurial.node import bin, hex
50 from mercurial.node import bin, hex
51 from mercurial.encoding import tolocal
51 from mercurial.encoding import tolocal
52 from mercurial.discovery import findcommonoutgoing
52 from mercurial.discovery import findcommonoutgoing
53 from mercurial.hg import peer
53 from mercurial.hg import peer
54 from mercurial.httppeer import makepeer
54 from mercurial.httppeer import makepeer
55 from mercurial.util import url as hg_url
55 from mercurial.util import url as hg_url
56 from mercurial.scmutil import revrange
56 from mercurial.scmutil import revrange
57 from mercurial.node import nullrev
57 from mercurial.node import nullrev
58 from mercurial import exchange
58 from mercurial import exchange
59 from hgext import largefiles
59 from hgext import largefiles
60
60
61 # those authnadlers are patched for python 2.6.5 bug an
61 # those authnadlers are patched for python 2.6.5 bug an
62 # infinit looping when given invalid resources
62 # infinit looping when given invalid resources
63 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
63 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
@@ -1,134 +1,134 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """
18 """
19 Adjustments to Mercurial
19 Adjustments to Mercurial
20
20
21 Intentionally kept separate from `hgcompat` and `hg`, so that these patches can
21 Intentionally kept separate from `hgcompat` and `hg`, so that these patches can
22 be applied without having to import the whole Mercurial machinery.
22 be applied without having to import the whole Mercurial machinery.
23
23
24 Imports are function local, so that just importing this module does not cause
24 Imports are function local, so that just importing this module does not cause
25 side-effects other than these functions being defined.
25 side-effects other than these functions being defined.
26 """
26 """
27
27
28 import logging
28 import logging
29
29
30
30
31 def patch_largefiles_capabilities():
31 def patch_largefiles_capabilities():
32 """
32 """
33 Patches the capabilities function in the largefiles extension.
33 Patches the capabilities function in the largefiles extension.
34 """
34 """
35 from vcsserver import hgcompat
35 from vcsserver import hgcompat
36 lfproto = hgcompat.largefiles.proto
36 lfproto = hgcompat.largefiles.proto
37 wrapper = _dynamic_capabilities_wrapper(
37 wrapper = _dynamic_capabilities_wrapper(
38 lfproto, hgcompat.extensions.extensions)
38 lfproto, hgcompat.extensions.extensions)
39 lfproto._capabilities = wrapper
39 lfproto._capabilities = wrapper
40
40
41
41
42 def _dynamic_capabilities_wrapper(lfproto, extensions):
42 def _dynamic_capabilities_wrapper(lfproto, extensions):
43
43
44 wrapped_capabilities = lfproto._capabilities
44 wrapped_capabilities = lfproto._capabilities
45 logger = logging.getLogger('vcsserver.hg')
45 logger = logging.getLogger('vcsserver.hg')
46
46
47 def _dynamic_capabilities(orig, repo, proto):
47 def _dynamic_capabilities(orig, repo, proto):
48 """
48 """
49 Adds dynamic behavior, so that the capability is only added if the
49 Adds dynamic behavior, so that the capability is only added if the
50 extension is enabled in the current ui object.
50 extension is enabled in the current ui object.
51 """
51 """
52 if 'largefiles' in dict(extensions(repo.ui)):
52 if 'largefiles' in dict(extensions(repo.ui)):
53 logger.debug('Extension largefiles enabled')
53 logger.debug('Extension largefiles enabled')
54 calc_capabilities = wrapped_capabilities
54 calc_capabilities = wrapped_capabilities
55 return calc_capabilities(orig, repo, proto)
55 return calc_capabilities(orig, repo, proto)
56 else:
56 else:
57 logger.debug('Extension largefiles disabled')
57 logger.debug('Extension largefiles disabled')
58 return orig(repo, proto)
58 return orig(repo, proto)
59
59
60 return _dynamic_capabilities
60 return _dynamic_capabilities
61
61
62
62
63 def patch_subrepo_type_mapping():
63 def patch_subrepo_type_mapping():
64 from collections import defaultdict
64 from collections import defaultdict
65 from hgcompat import subrepo
65 from hgcompat import subrepo
66 from exceptions import SubrepoMergeException
66 from vcsserver.exceptions import SubrepoMergeException
67
67
68 class NoOpSubrepo(subrepo.abstractsubrepo):
68 class NoOpSubrepo(subrepo.abstractsubrepo):
69
69
70 def __init__(self, ctx, path, *args, **kwargs):
70 def __init__(self, ctx, path, *args, **kwargs):
71 """Initialize abstractsubrepo part
71 """Initialize abstractsubrepo part
72
72
73 ``ctx`` is the context referring this subrepository in the
73 ``ctx`` is the context referring this subrepository in the
74 parent repository.
74 parent repository.
75
75
76 ``path`` is the path to this subrepository as seen from
76 ``path`` is the path to this subrepository as seen from
77 innermost repository.
77 innermost repository.
78 """
78 """
79 self.ui = ctx.repo().ui
79 self.ui = ctx.repo().ui
80 self._ctx = ctx
80 self._ctx = ctx
81 self._path = path
81 self._path = path
82
82
83 def storeclean(self, path):
83 def storeclean(self, path):
84 """
84 """
85 returns true if the repository has not changed since it was last
85 returns true if the repository has not changed since it was last
86 cloned from or pushed to a given repository.
86 cloned from or pushed to a given repository.
87 """
87 """
88 return True
88 return True
89
89
90 def dirty(self, ignoreupdate=False, missing=False):
90 def dirty(self, ignoreupdate=False, missing=False):
91 """returns true if the dirstate of the subrepo is dirty or does not
91 """returns true if the dirstate of the subrepo is dirty or does not
92 match current stored state. If ignoreupdate is true, only check
92 match current stored state. If ignoreupdate is true, only check
93 whether the subrepo has uncommitted changes in its dirstate.
93 whether the subrepo has uncommitted changes in its dirstate.
94 """
94 """
95 return False
95 return False
96
96
97 def basestate(self):
97 def basestate(self):
98 """current working directory base state, disregarding .hgsubstate
98 """current working directory base state, disregarding .hgsubstate
99 state and working directory modifications"""
99 state and working directory modifications"""
100 substate = subrepo.state(self._ctx, self.ui)
100 substate = subrepo.state(self._ctx, self.ui)
101 file_system_path, rev, repotype = substate.get(self._path)
101 file_system_path, rev, repotype = substate.get(self._path)
102 return rev
102 return rev
103
103
104 def remove(self):
104 def remove(self):
105 """remove the subrepo
105 """remove the subrepo
106
106
107 (should verify the dirstate is not dirty first)
107 (should verify the dirstate is not dirty first)
108 """
108 """
109 pass
109 pass
110
110
111 def get(self, state, overwrite=False):
111 def get(self, state, overwrite=False):
112 """run whatever commands are needed to put the subrepo into
112 """run whatever commands are needed to put the subrepo into
113 this state
113 this state
114 """
114 """
115 pass
115 pass
116
116
117 def merge(self, state):
117 def merge(self, state):
118 """merge currently-saved state with the new state."""
118 """merge currently-saved state with the new state."""
119 raise SubrepoMergeException()()
119 raise SubrepoMergeException()()
120
120
121 def push(self, opts):
121 def push(self, opts):
122 """perform whatever action is analogous to 'hg push'
122 """perform whatever action is analogous to 'hg push'
123
123
124 This may be a no-op on some systems.
124 This may be a no-op on some systems.
125 """
125 """
126 pass
126 pass
127
127
128 # Patch subrepo type mapping to always return our NoOpSubrepo class
128 # Patch subrepo type mapping to always return our NoOpSubrepo class
129 # whenever a subrepo class is looked up.
129 # whenever a subrepo class is looked up.
130 subrepo.types = {
130 subrepo.types = {
131 'hg': NoOpSubrepo,
131 'hg': NoOpSubrepo,
132 'git': NoOpSubrepo,
132 'git': NoOpSubrepo,
133 'svn': NoOpSubrepo
133 'svn': NoOpSubrepo
134 }
134 }
@@ -1,154 +1,203 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import re
20 import re
21 import os
21 import os
22 import sys
22 import sys
23 import datetime
23 import datetime
24 import logging
24 import logging
25 import pkg_resources
25 import pkg_resources
26
26
27 import vcsserver
27 import vcsserver
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 def get_git_hooks_path(repo_path, bare):
33 hooks_path = os.path.join(repo_path, 'hooks')
34 if not bare:
35 hooks_path = os.path.join(repo_path, '.git', 'hooks')
36
37 return hooks_path
38
39
32 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
40 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
33 """
41 """
34 Creates a RhodeCode hook inside a git repository
42 Creates a RhodeCode hook inside a git repository
35
43
36 :param repo_path: path to repository
44 :param repo_path: path to repository
37 :param executable: binary executable to put in the hooks
45 :param executable: binary executable to put in the hooks
38 :param force_create: Create even if same name hook exists
46 :param force_create: Create even if same name hook exists
39 """
47 """
40 executable = executable or sys.executable
48 executable = executable or sys.executable
41 hooks_path = os.path.join(repo_path, 'hooks')
49 hooks_path = get_git_hooks_path(repo_path, bare)
42 if not bare:
50
43 hooks_path = os.path.join(repo_path, '.git', 'hooks')
44 if not os.path.isdir(hooks_path):
51 if not os.path.isdir(hooks_path):
45 os.makedirs(hooks_path, mode=0o777)
52 os.makedirs(hooks_path, mode=0o777)
46
53
47 tmpl_post = pkg_resources.resource_string(
54 tmpl_post = pkg_resources.resource_string(
48 'vcsserver', '/'.join(
55 'vcsserver', '/'.join(
49 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
56 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
50 tmpl_pre = pkg_resources.resource_string(
57 tmpl_pre = pkg_resources.resource_string(
51 'vcsserver', '/'.join(
58 'vcsserver', '/'.join(
52 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
59 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
53
60
54 path = '' # not used for now
61 path = '' # not used for now
55 timestamp = datetime.datetime.utcnow().isoformat()
62 timestamp = datetime.datetime.utcnow().isoformat()
56
63
57 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
64 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
58 log.debug('Installing git hook in repo %s', repo_path)
65 log.debug('Installing git hook in repo %s', repo_path)
59 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
66 _hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
60 _rhodecode_hook = check_rhodecode_hook(_hook_file)
67 _rhodecode_hook = check_rhodecode_hook(_hook_file)
61
68
62 if _rhodecode_hook or force_create:
69 if _rhodecode_hook or force_create:
63 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
70 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
64 try:
71 try:
65 with open(_hook_file, 'wb') as f:
72 with open(_hook_file, 'wb') as f:
66 template = template.replace(
73 template = template.replace(
67 '_TMPL_', vcsserver.__version__)
74 '_TMPL_', vcsserver.__version__)
68 template = template.replace('_DATE_', timestamp)
75 template = template.replace('_DATE_', timestamp)
69 template = template.replace('_ENV_', executable)
76 template = template.replace('_ENV_', executable)
70 template = template.replace('_PATH_', path)
77 template = template.replace('_PATH_', path)
71 f.write(template)
78 f.write(template)
72 os.chmod(_hook_file, 0o755)
79 os.chmod(_hook_file, 0o755)
73 except IOError:
80 except IOError:
74 log.exception('error writing hook file %s', _hook_file)
81 log.exception('error writing hook file %s', _hook_file)
75 else:
82 else:
76 log.debug('skipping writing hook file')
83 log.debug('skipping writing hook file')
77
84
78 return True
85 return True
79
86
80
87
88 def get_svn_hooks_path(repo_path):
89 hooks_path = os.path.join(repo_path, 'hooks')
90
91 return hooks_path
92
93
81 def install_svn_hooks(repo_path, executable=None, force_create=False):
94 def install_svn_hooks(repo_path, executable=None, force_create=False):
82 """
95 """
83 Creates RhodeCode hooks inside a svn repository
96 Creates RhodeCode hooks inside a svn repository
84
97
85 :param repo_path: path to repository
98 :param repo_path: path to repository
86 :param executable: binary executable to put in the hooks
99 :param executable: binary executable to put in the hooks
87 :param force_create: Create even if same name hook exists
100 :param force_create: Create even if same name hook exists
88 """
101 """
89 executable = executable or sys.executable
102 executable = executable or sys.executable
90 hooks_path = os.path.join(repo_path, 'hooks')
103 hooks_path = get_svn_hooks_path(repo_path)
91 if not os.path.isdir(hooks_path):
104 if not os.path.isdir(hooks_path):
92 os.makedirs(hooks_path, mode=0o777)
105 os.makedirs(hooks_path, mode=0o777)
93
106
94 tmpl_post = pkg_resources.resource_string(
107 tmpl_post = pkg_resources.resource_string(
95 'vcsserver', '/'.join(
108 'vcsserver', '/'.join(
96 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
109 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
97 tmpl_pre = pkg_resources.resource_string(
110 tmpl_pre = pkg_resources.resource_string(
98 'vcsserver', '/'.join(
111 'vcsserver', '/'.join(
99 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
112 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
100
113
101 path = '' # not used for now
114 path = '' # not used for now
102 timestamp = datetime.datetime.utcnow().isoformat()
115 timestamp = datetime.datetime.utcnow().isoformat()
103
116
104 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
117 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
105 log.debug('Installing svn hook in repo %s', repo_path)
118 log.debug('Installing svn hook in repo %s', repo_path)
106 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
119 _hook_file = os.path.join(hooks_path, '%s-commit' % h_type)
107 _rhodecode_hook = check_rhodecode_hook(_hook_file)
120 _rhodecode_hook = check_rhodecode_hook(_hook_file)
108
121
109 if _rhodecode_hook or force_create:
122 if _rhodecode_hook or force_create:
110 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
123 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
111
124
112 try:
125 try:
113 with open(_hook_file, 'wb') as f:
126 with open(_hook_file, 'wb') as f:
114 template = template.replace(
127 template = template.replace(
115 '_TMPL_', vcsserver.__version__)
128 '_TMPL_', vcsserver.__version__)
116 template = template.replace('_DATE_', timestamp)
129 template = template.replace('_DATE_', timestamp)
117 template = template.replace('_ENV_', executable)
130 template = template.replace('_ENV_', executable)
118 template = template.replace('_PATH_', path)
131 template = template.replace('_PATH_', path)
119
132
120 f.write(template)
133 f.write(template)
121 os.chmod(_hook_file, 0o755)
134 os.chmod(_hook_file, 0o755)
122 except IOError:
135 except IOError:
123 log.exception('error writing hook file %s', _hook_file)
136 log.exception('error writing hook file %s', _hook_file)
124 else:
137 else:
125 log.debug('skipping writing hook file')
138 log.debug('skipping writing hook file')
126
139
127 return True
140 return True
128
141
129
142
143 def get_version_from_hook(hook_path):
144 version = ''
145 hook_content = read_hook_content(hook_path)
146 matches = re.search(r'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
147 if matches:
148 try:
149 version = matches.groups()[0]
150 log.debug('got version %s from hooks.', version)
151 except Exception:
152 log.exception("Exception while reading the hook version.")
153 return version.replace("'", "")
154
155
130 def check_rhodecode_hook(hook_path):
156 def check_rhodecode_hook(hook_path):
131 """
157 """
132 Check if the hook was created by RhodeCode
158 Check if the hook was created by RhodeCode
133 """
159 """
134 if not os.path.exists(hook_path):
160 if not os.path.exists(hook_path):
135 return True
161 return True
136
162
137 log.debug('hook exists, checking if it is from rhodecode')
163 log.debug('hook exists, checking if it is from RhodeCode')
138 hook_content = read_hook_content(hook_path)
164
139 matches = re.search(r'(?:RC_HOOK_VER)\s*=\s*(.*)', hook_content)
165 version = get_version_from_hook(hook_path)
140 if matches:
166 if version:
141 try:
142 version = matches.groups()[0]
143 log.debug('got version %s from hooks.', version)
144 return True
167 return True
145 except Exception:
146 log.exception("Exception while reading the hook version.")
147
168
148 return False
169 return False
149
170
150
171
151 def read_hook_content(hook_path):
172 def read_hook_content(hook_path):
152 with open(hook_path, 'rb') as f:
173 with open(hook_path, 'rb') as f:
153 content = f.read()
174 content = f.read()
154 return content
175 return content
176
177
178 def get_git_pre_hook_version(repo_path, bare):
179 hooks_path = get_git_hooks_path(repo_path, bare)
180 _hook_file = os.path.join(hooks_path, 'pre-receive')
181 version = get_version_from_hook(_hook_file)
182 return version
183
184
185 def get_git_post_hook_version(repo_path, bare):
186 hooks_path = get_git_hooks_path(repo_path, bare)
187 _hook_file = os.path.join(hooks_path, 'post-receive')
188 version = get_version_from_hook(_hook_file)
189 return version
190
191
192 def get_svn_pre_hook_version(repo_path):
193 hooks_path = get_svn_hooks_path(repo_path)
194 _hook_file = os.path.join(hooks_path, 'pre-commit')
195 version = get_version_from_hook(_hook_file)
196 return version
197
198
199 def get_svn_post_hook_version(repo_path):
200 hooks_path = get_svn_hooks_path(repo_path)
201 _hook_file = os.path.join(hooks_path, 'post-commit')
202 version = get_version_from_hook(_hook_file)
203 return version
@@ -1,711 +1,710 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import io
20 import io
21 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import base64
26 import base64
27
27
28 from httplib import HTTPConnection
28 from httplib import HTTPConnection
29
29
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33 import simplejson as json
33 import simplejson as json
34
34
35 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver import exceptions, subprocessio, settings
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class HooksHttpClient(object):
40 class HooksHttpClient(object):
41 connection = None
41 connection = None
42
42
43 def __init__(self, hooks_uri):
43 def __init__(self, hooks_uri):
44 self.hooks_uri = hooks_uri
44 self.hooks_uri = hooks_uri
45
45
46 def __call__(self, method, extras):
46 def __call__(self, method, extras):
47 connection = HTTPConnection(self.hooks_uri)
47 connection = HTTPConnection(self.hooks_uri)
48 body = self._serialize(method, extras)
48 body = self._serialize(method, extras)
49 try:
49 try:
50 connection.request('POST', '/', body)
50 connection.request('POST', '/', body)
51 except Exception:
51 except Exception:
52 log.error('Connection failed on %s', connection)
52 log.error('Connection failed on %s', connection)
53 raise
53 raise
54 response = connection.getresponse()
54 response = connection.getresponse()
55
55
56 response_data = response.read()
56 response_data = response.read()
57
57
58 try:
58 try:
59 return json.loads(response_data)
59 return json.loads(response_data)
60 except Exception:
60 except Exception:
61 log.exception('Failed to decode hook response json data. '
61 log.exception('Failed to decode hook response json data. '
62 'response_code:%s, raw_data:%s',
62 'response_code:%s, raw_data:%s',
63 response.status, response_data)
63 response.status, response_data)
64 raise
64 raise
65
65
66 def _serialize(self, hook_name, extras):
66 def _serialize(self, hook_name, extras):
67 data = {
67 data = {
68 'method': hook_name,
68 'method': hook_name,
69 'extras': extras
69 'extras': extras
70 }
70 }
71 return json.dumps(data)
71 return json.dumps(data)
72
72
73
73
74 class HooksDummyClient(object):
74 class HooksDummyClient(object):
75 def __init__(self, hooks_module):
75 def __init__(self, hooks_module):
76 self._hooks_module = importlib.import_module(hooks_module)
76 self._hooks_module = importlib.import_module(hooks_module)
77
77
78 def __call__(self, hook_name, extras):
78 def __call__(self, hook_name, extras):
79 with self._hooks_module.Hooks() as hooks:
79 with self._hooks_module.Hooks() as hooks:
80 return getattr(hooks, hook_name)(extras)
80 return getattr(hooks, hook_name)(extras)
81
81
82
82
83 class RemoteMessageWriter(object):
83 class RemoteMessageWriter(object):
84 """Writer base class."""
84 """Writer base class."""
85 def write(self, message):
85 def write(self, message):
86 raise NotImplementedError()
86 raise NotImplementedError()
87
87
88
88
89 class HgMessageWriter(RemoteMessageWriter):
89 class HgMessageWriter(RemoteMessageWriter):
90 """Writer that knows how to send messages to mercurial clients."""
90 """Writer that knows how to send messages to mercurial clients."""
91
91
92 def __init__(self, ui):
92 def __init__(self, ui):
93 self.ui = ui
93 self.ui = ui
94
94
95 def write(self, message):
95 def write(self, message):
96 # TODO: Check why the quiet flag is set by default.
96 # TODO: Check why the quiet flag is set by default.
97 old = self.ui.quiet
97 old = self.ui.quiet
98 self.ui.quiet = False
98 self.ui.quiet = False
99 self.ui.status(message.encode('utf-8'))
99 self.ui.status(message.encode('utf-8'))
100 self.ui.quiet = old
100 self.ui.quiet = old
101
101
102
102
103 class GitMessageWriter(RemoteMessageWriter):
103 class GitMessageWriter(RemoteMessageWriter):
104 """Writer that knows how to send messages to git clients."""
104 """Writer that knows how to send messages to git clients."""
105
105
106 def __init__(self, stdout=None):
106 def __init__(self, stdout=None):
107 self.stdout = stdout or sys.stdout
107 self.stdout = stdout or sys.stdout
108
108
109 def write(self, message):
109 def write(self, message):
110 self.stdout.write(message.encode('utf-8'))
110 self.stdout.write(message.encode('utf-8'))
111
111
112
112
113 class SvnMessageWriter(RemoteMessageWriter):
113 class SvnMessageWriter(RemoteMessageWriter):
114 """Writer that knows how to send messages to svn clients."""
114 """Writer that knows how to send messages to svn clients."""
115
115
116 def __init__(self, stderr=None):
116 def __init__(self, stderr=None):
117 # SVN needs data sent to stderr for back-to-client messaging
117 # SVN needs data sent to stderr for back-to-client messaging
118 self.stderr = stderr or sys.stderr
118 self.stderr = stderr or sys.stderr
119
119
120 def write(self, message):
120 def write(self, message):
121 self.stderr.write(message.encode('utf-8'))
121 self.stderr.write(message.encode('utf-8'))
122
122
123
123
124 def _handle_exception(result):
124 def _handle_exception(result):
125 exception_class = result.get('exception')
125 exception_class = result.get('exception')
126 exception_traceback = result.get('exception_traceback')
126 exception_traceback = result.get('exception_traceback')
127
127
128 if exception_traceback:
128 if exception_traceback:
129 log.error('Got traceback from remote call:%s', exception_traceback)
129 log.error('Got traceback from remote call:%s', exception_traceback)
130
130
131 if exception_class == 'HTTPLockedRC':
131 if exception_class == 'HTTPLockedRC':
132 raise exceptions.RepositoryLockedException()(*result['exception_args'])
132 raise exceptions.RepositoryLockedException()(*result['exception_args'])
133 elif exception_class == 'HTTPBranchProtected':
133 elif exception_class == 'HTTPBranchProtected':
134 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
134 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
135 elif exception_class == 'RepositoryError':
135 elif exception_class == 'RepositoryError':
136 raise exceptions.VcsException()(*result['exception_args'])
136 raise exceptions.VcsException()(*result['exception_args'])
137 elif exception_class:
137 elif exception_class:
138 raise Exception('Got remote exception "%s" with args "%s"' %
138 raise Exception('Got remote exception "%s" with args "%s"' %
139 (exception_class, result['exception_args']))
139 (exception_class, result['exception_args']))
140
140
141
141
142 def _get_hooks_client(extras):
142 def _get_hooks_client(extras):
143 if 'hooks_uri' in extras:
143 if 'hooks_uri' in extras:
144 protocol = extras.get('hooks_protocol')
144 protocol = extras.get('hooks_protocol')
145 return HooksHttpClient(extras['hooks_uri'])
145 return HooksHttpClient(extras['hooks_uri'])
146 else:
146 else:
147 return HooksDummyClient(extras['hooks_module'])
147 return HooksDummyClient(extras['hooks_module'])
148
148
149
149
150 def _call_hook(hook_name, extras, writer):
150 def _call_hook(hook_name, extras, writer):
151 hooks_client = _get_hooks_client(extras)
151 hooks_client = _get_hooks_client(extras)
152 log.debug('Hooks, using client:%s', hooks_client)
152 log.debug('Hooks, using client:%s', hooks_client)
153 result = hooks_client(hook_name, extras)
153 result = hooks_client(hook_name, extras)
154 log.debug('Hooks got result: %s', result)
154 log.debug('Hooks got result: %s', result)
155
155
156 _handle_exception(result)
156 _handle_exception(result)
157 writer.write(result['output'])
157 writer.write(result['output'])
158
158
159 return result['status']
159 return result['status']
160
160
161
161
162 def _extras_from_ui(ui):
162 def _extras_from_ui(ui):
163 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
163 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
164 if not hook_data:
164 if not hook_data:
165 # maybe it's inside environ ?
165 # maybe it's inside environ ?
166 env_hook_data = os.environ.get('RC_SCM_DATA')
166 env_hook_data = os.environ.get('RC_SCM_DATA')
167 if env_hook_data:
167 if env_hook_data:
168 hook_data = env_hook_data
168 hook_data = env_hook_data
169
169
170 extras = {}
170 extras = {}
171 if hook_data:
171 if hook_data:
172 extras = json.loads(hook_data)
172 extras = json.loads(hook_data)
173 return extras
173 return extras
174
174
175
175
176 def _rev_range_hash(repo, node, check_heads=False):
176 def _rev_range_hash(repo, node, check_heads=False):
177
177
178 commits = []
178 commits = []
179 revs = []
179 revs = []
180 start = repo[node].rev()
180 start = repo[node].rev()
181 end = len(repo)
181 end = len(repo)
182 for rev in range(start, end):
182 for rev in range(start, end):
183 revs.append(rev)
183 revs.append(rev)
184 ctx = repo[rev]
184 ctx = repo[rev]
185 commit_id = mercurial.node.hex(ctx.node())
185 commit_id = mercurial.node.hex(ctx.node())
186 branch = ctx.branch()
186 branch = ctx.branch()
187 commits.append((commit_id, branch))
187 commits.append((commit_id, branch))
188
188
189 parent_heads = []
189 parent_heads = []
190 if check_heads:
190 if check_heads:
191 parent_heads = _check_heads(repo, start, end, revs)
191 parent_heads = _check_heads(repo, start, end, revs)
192 return commits, parent_heads
192 return commits, parent_heads
193
193
194
194
195 def _check_heads(repo, start, end, commits):
195 def _check_heads(repo, start, end, commits):
196 changelog = repo.changelog
196 changelog = repo.changelog
197 parents = set()
197 parents = set()
198
198
199 for new_rev in commits:
199 for new_rev in commits:
200 for p in changelog.parentrevs(new_rev):
200 for p in changelog.parentrevs(new_rev):
201 if p == mercurial.node.nullrev:
201 if p == mercurial.node.nullrev:
202 continue
202 continue
203 if p < start:
203 if p < start:
204 parents.add(p)
204 parents.add(p)
205
205
206 for p in parents:
206 for p in parents:
207 branch = repo[p].branch()
207 branch = repo[p].branch()
208 # The heads descending from that parent, on the same branch
208 # The heads descending from that parent, on the same branch
209 parent_heads = set([p])
209 parent_heads = set([p])
210 reachable = set([p])
210 reachable = set([p])
211 for x in xrange(p + 1, end):
211 for x in xrange(p + 1, end):
212 if repo[x].branch() != branch:
212 if repo[x].branch() != branch:
213 continue
213 continue
214 for pp in changelog.parentrevs(x):
214 for pp in changelog.parentrevs(x):
215 if pp in reachable:
215 if pp in reachable:
216 reachable.add(x)
216 reachable.add(x)
217 parent_heads.discard(pp)
217 parent_heads.discard(pp)
218 parent_heads.add(x)
218 parent_heads.add(x)
219 # More than one head? Suggest merging
219 # More than one head? Suggest merging
220 if len(parent_heads) > 1:
220 if len(parent_heads) > 1:
221 return list(parent_heads)
221 return list(parent_heads)
222
222
223 return []
223 return []
224
224
225
225
226 def _get_git_env():
226 def _get_git_env():
227 env = {}
227 env = {}
228 for k, v in os.environ.items():
228 for k, v in os.environ.items():
229 if k.startswith('GIT'):
229 if k.startswith('GIT'):
230 env[k] = v
230 env[k] = v
231
231
232 # serialized version
232 # serialized version
233 return [(k, v) for k, v in env.items()]
233 return [(k, v) for k, v in env.items()]
234
234
235
235
236 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
236 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
237 env = {}
237 env = {}
238 for k, v in os.environ.items():
238 for k, v in os.environ.items():
239 if k.startswith('HG'):
239 if k.startswith('HG'):
240 env[k] = v
240 env[k] = v
241
241
242 env['HG_NODE'] = old_rev
242 env['HG_NODE'] = old_rev
243 env['HG_NODE_LAST'] = new_rev
243 env['HG_NODE_LAST'] = new_rev
244 env['HG_TXNID'] = txnid
244 env['HG_TXNID'] = txnid
245 env['HG_PENDING'] = repo_path
245 env['HG_PENDING'] = repo_path
246
246
247 return [(k, v) for k, v in env.items()]
247 return [(k, v) for k, v in env.items()]
248
248
249
249
250 def repo_size(ui, repo, **kwargs):
250 def repo_size(ui, repo, **kwargs):
251 extras = _extras_from_ui(ui)
251 extras = _extras_from_ui(ui)
252 return _call_hook('repo_size', extras, HgMessageWriter(ui))
252 return _call_hook('repo_size', extras, HgMessageWriter(ui))
253
253
254
254
255 def pre_pull(ui, repo, **kwargs):
255 def pre_pull(ui, repo, **kwargs):
256 extras = _extras_from_ui(ui)
256 extras = _extras_from_ui(ui)
257 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
257 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
258
258
259
259
260 def pre_pull_ssh(ui, repo, **kwargs):
260 def pre_pull_ssh(ui, repo, **kwargs):
261 extras = _extras_from_ui(ui)
261 extras = _extras_from_ui(ui)
262 if extras and extras.get('SSH'):
262 if extras and extras.get('SSH'):
263 return pre_pull(ui, repo, **kwargs)
263 return pre_pull(ui, repo, **kwargs)
264 return 0
264 return 0
265
265
266
266
267 def post_pull(ui, repo, **kwargs):
267 def post_pull(ui, repo, **kwargs):
268 extras = _extras_from_ui(ui)
268 extras = _extras_from_ui(ui)
269 return _call_hook('post_pull', extras, HgMessageWriter(ui))
269 return _call_hook('post_pull', extras, HgMessageWriter(ui))
270
270
271
271
272 def post_pull_ssh(ui, repo, **kwargs):
272 def post_pull_ssh(ui, repo, **kwargs):
273 extras = _extras_from_ui(ui)
273 extras = _extras_from_ui(ui)
274 if extras and extras.get('SSH'):
274 if extras and extras.get('SSH'):
275 return post_pull(ui, repo, **kwargs)
275 return post_pull(ui, repo, **kwargs)
276 return 0
276 return 0
277
277
278
278
279 def pre_push(ui, repo, node=None, **kwargs):
279 def pre_push(ui, repo, node=None, **kwargs):
280 """
280 """
281 Mercurial pre_push hook
281 Mercurial pre_push hook
282 """
282 """
283 extras = _extras_from_ui(ui)
283 extras = _extras_from_ui(ui)
284 detect_force_push = extras.get('detect_force_push')
284 detect_force_push = extras.get('detect_force_push')
285
285
286 rev_data = []
286 rev_data = []
287 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
287 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
288 branches = collections.defaultdict(list)
288 branches = collections.defaultdict(list)
289 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
289 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
290 for commit_id, branch in commits:
290 for commit_id, branch in commits:
291 branches[branch].append(commit_id)
291 branches[branch].append(commit_id)
292
292
293 for branch, commits in branches.items():
293 for branch, commits in branches.items():
294 old_rev = kwargs.get('node_last') or commits[0]
294 old_rev = kwargs.get('node_last') or commits[0]
295 rev_data.append({
295 rev_data.append({
296 'total_commits': len(commits),
296 'total_commits': len(commits),
297 'old_rev': old_rev,
297 'old_rev': old_rev,
298 'new_rev': commits[-1],
298 'new_rev': commits[-1],
299 'ref': '',
299 'ref': '',
300 'type': 'branch',
300 'type': 'branch',
301 'name': branch,
301 'name': branch,
302 })
302 })
303
303
304 for push_ref in rev_data:
304 for push_ref in rev_data:
305 push_ref['multiple_heads'] = _heads
305 push_ref['multiple_heads'] = _heads
306
306
307 repo_path = os.path.join(
307 repo_path = os.path.join(
308 extras.get('repo_store', ''), extras.get('repository', ''))
308 extras.get('repo_store', ''), extras.get('repository', ''))
309 push_ref['hg_env'] = _get_hg_env(
309 push_ref['hg_env'] = _get_hg_env(
310 old_rev=push_ref['old_rev'],
310 old_rev=push_ref['old_rev'],
311 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
311 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
312 repo_path=repo_path)
312 repo_path=repo_path)
313
313
314 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
314 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
315 extras['commit_ids'] = rev_data
315 extras['commit_ids'] = rev_data
316
316
317 return _call_hook('pre_push', extras, HgMessageWriter(ui))
317 return _call_hook('pre_push', extras, HgMessageWriter(ui))
318
318
319
319
320 def pre_push_ssh(ui, repo, node=None, **kwargs):
320 def pre_push_ssh(ui, repo, node=None, **kwargs):
321 extras = _extras_from_ui(ui)
321 extras = _extras_from_ui(ui)
322 if extras.get('SSH'):
322 if extras.get('SSH'):
323 return pre_push(ui, repo, node, **kwargs)
323 return pre_push(ui, repo, node, **kwargs)
324
324
325 return 0
325 return 0
326
326
327
327
328 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
328 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
329 """
329 """
330 Mercurial pre_push hook for SSH
330 Mercurial pre_push hook for SSH
331 """
331 """
332 extras = _extras_from_ui(ui)
332 extras = _extras_from_ui(ui)
333 if extras.get('SSH'):
333 if extras.get('SSH'):
334 permission = extras['SSH_PERMISSIONS']
334 permission = extras['SSH_PERMISSIONS']
335
335
336 if 'repository.write' == permission or 'repository.admin' == permission:
336 if 'repository.write' == permission or 'repository.admin' == permission:
337 return 0
337 return 0
338
338
339 # non-zero ret code
339 # non-zero ret code
340 return 1
340 return 1
341
341
342 return 0
342 return 0
343
343
344
344
345 def post_push(ui, repo, node, **kwargs):
345 def post_push(ui, repo, node, **kwargs):
346 """
346 """
347 Mercurial post_push hook
347 Mercurial post_push hook
348 """
348 """
349 extras = _extras_from_ui(ui)
349 extras = _extras_from_ui(ui)
350
350
351 commit_ids = []
351 commit_ids = []
352 branches = []
352 branches = []
353 bookmarks = []
353 bookmarks = []
354 tags = []
354 tags = []
355
355
356 commits, _heads = _rev_range_hash(repo, node)
356 commits, _heads = _rev_range_hash(repo, node)
357 for commit_id, branch in commits:
357 for commit_id, branch in commits:
358 commit_ids.append(commit_id)
358 commit_ids.append(commit_id)
359 if branch not in branches:
359 if branch not in branches:
360 branches.append(branch)
360 branches.append(branch)
361
361
362 if hasattr(ui, '_rc_pushkey_branches'):
362 if hasattr(ui, '_rc_pushkey_branches'):
363 bookmarks = ui._rc_pushkey_branches
363 bookmarks = ui._rc_pushkey_branches
364
364
365 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
365 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
366 extras['commit_ids'] = commit_ids
366 extras['commit_ids'] = commit_ids
367 extras['new_refs'] = {
367 extras['new_refs'] = {
368 'branches': branches,
368 'branches': branches,
369 'bookmarks': bookmarks,
369 'bookmarks': bookmarks,
370 'tags': tags
370 'tags': tags
371 }
371 }
372
372
373 return _call_hook('post_push', extras, HgMessageWriter(ui))
373 return _call_hook('post_push', extras, HgMessageWriter(ui))
374
374
375
375
376 def post_push_ssh(ui, repo, node, **kwargs):
376 def post_push_ssh(ui, repo, node, **kwargs):
377 """
377 """
378 Mercurial post_push hook for SSH
378 Mercurial post_push hook for SSH
379 """
379 """
380 if _extras_from_ui(ui).get('SSH'):
380 if _extras_from_ui(ui).get('SSH'):
381 return post_push(ui, repo, node, **kwargs)
381 return post_push(ui, repo, node, **kwargs)
382 return 0
382 return 0
383
383
384
384
385 def key_push(ui, repo, **kwargs):
385 def key_push(ui, repo, **kwargs):
386 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
386 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
387 # store new bookmarks in our UI object propagated later to post_push
387 # store new bookmarks in our UI object propagated later to post_push
388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
389 return
389 return
390
390
391
391
392 # backward compat
392 # backward compat
393 log_pull_action = post_pull
393 log_pull_action = post_pull
394
394
395 # backward compat
395 # backward compat
396 log_push_action = post_push
396 log_push_action = post_push
397
397
398
398
399 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
399 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
400 """
400 """
401 Old hook name: keep here for backward compatibility.
401 Old hook name: keep here for backward compatibility.
402
402
403 This is only required when the installed git hooks are not upgraded.
403 This is only required when the installed git hooks are not upgraded.
404 """
404 """
405 pass
405 pass
406
406
407
407
408 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
408 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
409 """
409 """
410 Old hook name: keep here for backward compatibility.
410 Old hook name: keep here for backward compatibility.
411
411
412 This is only required when the installed git hooks are not upgraded.
412 This is only required when the installed git hooks are not upgraded.
413 """
413 """
414 pass
414 pass
415
415
416
416
417 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
417 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
418
418
419
419
420 def git_pre_pull(extras):
420 def git_pre_pull(extras):
421 """
421 """
422 Pre pull hook.
422 Pre pull hook.
423
423
424 :param extras: dictionary containing the keys defined in simplevcs
424 :param extras: dictionary containing the keys defined in simplevcs
425 :type extras: dict
425 :type extras: dict
426
426
427 :return: status code of the hook. 0 for success.
427 :return: status code of the hook. 0 for success.
428 :rtype: int
428 :rtype: int
429 """
429 """
430 if 'pull' not in extras['hooks']:
430 if 'pull' not in extras['hooks']:
431 return HookResponse(0, '')
431 return HookResponse(0, '')
432
432
433 stdout = io.BytesIO()
433 stdout = io.BytesIO()
434 try:
434 try:
435 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
435 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
436 except Exception as error:
436 except Exception as error:
437 status = 128
437 status = 128
438 stdout.write('ERROR: %s\n' % str(error))
438 stdout.write('ERROR: %s\n' % str(error))
439
439
440 return HookResponse(status, stdout.getvalue())
440 return HookResponse(status, stdout.getvalue())
441
441
442
442
443 def git_post_pull(extras):
443 def git_post_pull(extras):
444 """
444 """
445 Post pull hook.
445 Post pull hook.
446
446
447 :param extras: dictionary containing the keys defined in simplevcs
447 :param extras: dictionary containing the keys defined in simplevcs
448 :type extras: dict
448 :type extras: dict
449
449
450 :return: status code of the hook. 0 for success.
450 :return: status code of the hook. 0 for success.
451 :rtype: int
451 :rtype: int
452 """
452 """
453 if 'pull' not in extras['hooks']:
453 if 'pull' not in extras['hooks']:
454 return HookResponse(0, '')
454 return HookResponse(0, '')
455
455
456 stdout = io.BytesIO()
456 stdout = io.BytesIO()
457 try:
457 try:
458 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
458 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
459 except Exception as error:
459 except Exception as error:
460 status = 128
460 status = 128
461 stdout.write('ERROR: %s\n' % error)
461 stdout.write('ERROR: %s\n' % error)
462
462
463 return HookResponse(status, stdout.getvalue())
463 return HookResponse(status, stdout.getvalue())
464
464
465
465
466 def _parse_git_ref_lines(revision_lines):
466 def _parse_git_ref_lines(revision_lines):
467 rev_data = []
467 rev_data = []
468 for revision_line in revision_lines or []:
468 for revision_line in revision_lines or []:
469 old_rev, new_rev, ref = revision_line.strip().split(' ')
469 old_rev, new_rev, ref = revision_line.strip().split(' ')
470 ref_data = ref.split('/', 2)
470 ref_data = ref.split('/', 2)
471 if ref_data[1] in ('tags', 'heads'):
471 if ref_data[1] in ('tags', 'heads'):
472 rev_data.append({
472 rev_data.append({
473 # NOTE(marcink):
473 # NOTE(marcink):
474 # we're unable to tell total_commits for git at this point
474 # we're unable to tell total_commits for git at this point
475 # but we set the variable for consistency with GIT
475 # but we set the variable for consistency with GIT
476 'total_commits': -1,
476 'total_commits': -1,
477 'old_rev': old_rev,
477 'old_rev': old_rev,
478 'new_rev': new_rev,
478 'new_rev': new_rev,
479 'ref': ref,
479 'ref': ref,
480 'type': ref_data[1],
480 'type': ref_data[1],
481 'name': ref_data[2],
481 'name': ref_data[2],
482 })
482 })
483 return rev_data
483 return rev_data
484
484
485
485
486 def git_pre_receive(unused_repo_path, revision_lines, env):
486 def git_pre_receive(unused_repo_path, revision_lines, env):
487 """
487 """
488 Pre push hook.
488 Pre push hook.
489
489
490 :param extras: dictionary containing the keys defined in simplevcs
490 :param extras: dictionary containing the keys defined in simplevcs
491 :type extras: dict
491 :type extras: dict
492
492
493 :return: status code of the hook. 0 for success.
493 :return: status code of the hook. 0 for success.
494 :rtype: int
494 :rtype: int
495 """
495 """
496 extras = json.loads(env['RC_SCM_DATA'])
496 extras = json.loads(env['RC_SCM_DATA'])
497 rev_data = _parse_git_ref_lines(revision_lines)
497 rev_data = _parse_git_ref_lines(revision_lines)
498 if 'push' not in extras['hooks']:
498 if 'push' not in extras['hooks']:
499 return 0
499 return 0
500 empty_commit_id = '0' * 40
500 empty_commit_id = '0' * 40
501
501
502 detect_force_push = extras.get('detect_force_push')
502 detect_force_push = extras.get('detect_force_push')
503
503
504 for push_ref in rev_data:
504 for push_ref in rev_data:
505 # store our git-env which holds the temp store
505 # store our git-env which holds the temp store
506 push_ref['git_env'] = _get_git_env()
506 push_ref['git_env'] = _get_git_env()
507 push_ref['pruned_sha'] = ''
507 push_ref['pruned_sha'] = ''
508 if not detect_force_push:
508 if not detect_force_push:
509 # don't check for forced-push when we don't need to
509 # don't check for forced-push when we don't need to
510 continue
510 continue
511
511
512 type_ = push_ref['type']
512 type_ = push_ref['type']
513 new_branch = push_ref['old_rev'] == empty_commit_id
513 new_branch = push_ref['old_rev'] == empty_commit_id
514 if type_ == 'heads' and not new_branch:
514 delete_branch = push_ref['new_rev'] == empty_commit_id
515 if type_ == 'heads' and not (new_branch or delete_branch):
515 old_rev = push_ref['old_rev']
516 old_rev = push_ref['old_rev']
516 new_rev = push_ref['new_rev']
517 new_rev = push_ref['new_rev']
517 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
518 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
518 old_rev, '^{}'.format(new_rev)]
519 stdout, stderr = subprocessio.run_command(
519 stdout, stderr = subprocessio.run_command(
520 cmd, env=os.environ.copy())
520 cmd, env=os.environ.copy())
521 # means we're having some non-reachable objects, this forced push
521 # means we're having some non-reachable objects, this forced push was used
522 # was used
523 if stdout:
522 if stdout:
524 push_ref['pruned_sha'] = stdout.splitlines()
523 push_ref['pruned_sha'] = stdout.splitlines()
525
524
526 extras['hook_type'] = 'pre_receive'
525 extras['hook_type'] = 'pre_receive'
527 extras['commit_ids'] = rev_data
526 extras['commit_ids'] = rev_data
528 return _call_hook('pre_push', extras, GitMessageWriter())
527 return _call_hook('pre_push', extras, GitMessageWriter())
529
528
530
529
531 def git_post_receive(unused_repo_path, revision_lines, env):
530 def git_post_receive(unused_repo_path, revision_lines, env):
532 """
531 """
533 Post push hook.
532 Post push hook.
534
533
535 :param extras: dictionary containing the keys defined in simplevcs
534 :param extras: dictionary containing the keys defined in simplevcs
536 :type extras: dict
535 :type extras: dict
537
536
538 :return: status code of the hook. 0 for success.
537 :return: status code of the hook. 0 for success.
539 :rtype: int
538 :rtype: int
540 """
539 """
541 extras = json.loads(env['RC_SCM_DATA'])
540 extras = json.loads(env['RC_SCM_DATA'])
542 if 'push' not in extras['hooks']:
541 if 'push' not in extras['hooks']:
543 return 0
542 return 0
544
543
545 rev_data = _parse_git_ref_lines(revision_lines)
544 rev_data = _parse_git_ref_lines(revision_lines)
546
545
547 git_revs = []
546 git_revs = []
548
547
549 # N.B.(skreft): it is ok to just call git, as git before calling a
548 # N.B.(skreft): it is ok to just call git, as git before calling a
550 # subcommand sets the PATH environment variable so that it point to the
549 # subcommand sets the PATH environment variable so that it point to the
551 # correct version of the git executable.
550 # correct version of the git executable.
552 empty_commit_id = '0' * 40
551 empty_commit_id = '0' * 40
553 branches = []
552 branches = []
554 tags = []
553 tags = []
555 for push_ref in rev_data:
554 for push_ref in rev_data:
556 type_ = push_ref['type']
555 type_ = push_ref['type']
557
556
558 if type_ == 'heads':
557 if type_ == 'heads':
559 if push_ref['old_rev'] == empty_commit_id:
558 if push_ref['old_rev'] == empty_commit_id:
560 # starting new branch case
559 # starting new branch case
561 if push_ref['name'] not in branches:
560 if push_ref['name'] not in branches:
562 branches.append(push_ref['name'])
561 branches.append(push_ref['name'])
563
562
564 # Fix up head revision if needed
563 # Fix up head revision if needed
565 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
564 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
566 try:
565 try:
567 subprocessio.run_command(cmd, env=os.environ.copy())
566 subprocessio.run_command(cmd, env=os.environ.copy())
568 except Exception:
567 except Exception:
569 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
568 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
570 'refs/heads/%s' % push_ref['name']]
569 'refs/heads/%s' % push_ref['name']]
571 print("Setting default branch to %s" % push_ref['name'])
570 print("Setting default branch to %s" % push_ref['name'])
572 subprocessio.run_command(cmd, env=os.environ.copy())
571 subprocessio.run_command(cmd, env=os.environ.copy())
573
572
574 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
573 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
575 '--format=%(refname)', 'refs/heads/*']
574 '--format=%(refname)', 'refs/heads/*']
576 stdout, stderr = subprocessio.run_command(
575 stdout, stderr = subprocessio.run_command(
577 cmd, env=os.environ.copy())
576 cmd, env=os.environ.copy())
578 heads = stdout
577 heads = stdout
579 heads = heads.replace(push_ref['ref'], '')
578 heads = heads.replace(push_ref['ref'], '')
580 heads = ' '.join(head for head
579 heads = ' '.join(head for head
581 in heads.splitlines() if head) or '.'
580 in heads.splitlines() if head) or '.'
582 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
581 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
583 '--pretty=format:%H', '--', push_ref['new_rev'],
582 '--pretty=format:%H', '--', push_ref['new_rev'],
584 '--not', heads]
583 '--not', heads]
585 stdout, stderr = subprocessio.run_command(
584 stdout, stderr = subprocessio.run_command(
586 cmd, env=os.environ.copy())
585 cmd, env=os.environ.copy())
587 git_revs.extend(stdout.splitlines())
586 git_revs.extend(stdout.splitlines())
588 elif push_ref['new_rev'] == empty_commit_id:
587 elif push_ref['new_rev'] == empty_commit_id:
589 # delete branch case
588 # delete branch case
590 git_revs.append('delete_branch=>%s' % push_ref['name'])
589 git_revs.append('delete_branch=>%s' % push_ref['name'])
591 else:
590 else:
592 if push_ref['name'] not in branches:
591 if push_ref['name'] not in branches:
593 branches.append(push_ref['name'])
592 branches.append(push_ref['name'])
594
593
595 cmd = [settings.GIT_EXECUTABLE, 'log',
594 cmd = [settings.GIT_EXECUTABLE, 'log',
596 '{old_rev}..{new_rev}'.format(**push_ref),
595 '{old_rev}..{new_rev}'.format(**push_ref),
597 '--reverse', '--pretty=format:%H']
596 '--reverse', '--pretty=format:%H']
598 stdout, stderr = subprocessio.run_command(
597 stdout, stderr = subprocessio.run_command(
599 cmd, env=os.environ.copy())
598 cmd, env=os.environ.copy())
600 git_revs.extend(stdout.splitlines())
599 git_revs.extend(stdout.splitlines())
601 elif type_ == 'tags':
600 elif type_ == 'tags':
602 if push_ref['name'] not in tags:
601 if push_ref['name'] not in tags:
603 tags.append(push_ref['name'])
602 tags.append(push_ref['name'])
604 git_revs.append('tag=>%s' % push_ref['name'])
603 git_revs.append('tag=>%s' % push_ref['name'])
605
604
606 extras['hook_type'] = 'post_receive'
605 extras['hook_type'] = 'post_receive'
607 extras['commit_ids'] = git_revs
606 extras['commit_ids'] = git_revs
608 extras['new_refs'] = {
607 extras['new_refs'] = {
609 'branches': branches,
608 'branches': branches,
610 'bookmarks': [],
609 'bookmarks': [],
611 'tags': tags,
610 'tags': tags,
612 }
611 }
613
612
614 if 'repo_size' in extras['hooks']:
613 if 'repo_size' in extras['hooks']:
615 try:
614 try:
616 _call_hook('repo_size', extras, GitMessageWriter())
615 _call_hook('repo_size', extras, GitMessageWriter())
617 except:
616 except:
618 pass
617 pass
619
618
620 return _call_hook('post_push', extras, GitMessageWriter())
619 return _call_hook('post_push', extras, GitMessageWriter())
621
620
622
621
623 def _get_extras_from_txn_id(path, txn_id):
622 def _get_extras_from_txn_id(path, txn_id):
624 extras = {}
623 extras = {}
625 try:
624 try:
626 cmd = ['svnlook', 'pget',
625 cmd = ['svnlook', 'pget',
627 '-t', txn_id,
626 '-t', txn_id,
628 '--revprop', path, 'rc-scm-extras']
627 '--revprop', path, 'rc-scm-extras']
629 stdout, stderr = subprocessio.run_command(
628 stdout, stderr = subprocessio.run_command(
630 cmd, env=os.environ.copy())
629 cmd, env=os.environ.copy())
631 extras = json.loads(base64.urlsafe_b64decode(stdout))
630 extras = json.loads(base64.urlsafe_b64decode(stdout))
632 except Exception:
631 except Exception:
633 log.exception('Failed to extract extras info from txn_id')
632 log.exception('Failed to extract extras info from txn_id')
634
633
635 return extras
634 return extras
636
635
637
636
638 def _get_extras_from_commit_id(commit_id, path):
637 def _get_extras_from_commit_id(commit_id, path):
639 extras = {}
638 extras = {}
640 try:
639 try:
641 cmd = ['svnlook', 'pget',
640 cmd = ['svnlook', 'pget',
642 '-r', commit_id,
641 '-r', commit_id,
643 '--revprop', path, 'rc-scm-extras']
642 '--revprop', path, 'rc-scm-extras']
644 stdout, stderr = subprocessio.run_command(
643 stdout, stderr = subprocessio.run_command(
645 cmd, env=os.environ.copy())
644 cmd, env=os.environ.copy())
646 extras = json.loads(base64.urlsafe_b64decode(stdout))
645 extras = json.loads(base64.urlsafe_b64decode(stdout))
647 except Exception:
646 except Exception:
648 log.exception('Failed to extract extras info from commit_id')
647 log.exception('Failed to extract extras info from commit_id')
649
648
650 return extras
649 return extras
651
650
652
651
653 def svn_pre_commit(repo_path, commit_data, env):
652 def svn_pre_commit(repo_path, commit_data, env):
654 path, txn_id = commit_data
653 path, txn_id = commit_data
655 branches = []
654 branches = []
656 tags = []
655 tags = []
657
656
658 if env.get('RC_SCM_DATA'):
657 if env.get('RC_SCM_DATA'):
659 extras = json.loads(env['RC_SCM_DATA'])
658 extras = json.loads(env['RC_SCM_DATA'])
660 else:
659 else:
661 # fallback method to read from TXN-ID stored data
660 # fallback method to read from TXN-ID stored data
662 extras = _get_extras_from_txn_id(path, txn_id)
661 extras = _get_extras_from_txn_id(path, txn_id)
663 if not extras:
662 if not extras:
664 return 0
663 return 0
665
664
666 extras['hook_type'] = 'pre_commit'
665 extras['hook_type'] = 'pre_commit'
667 extras['commit_ids'] = []
666 extras['commit_ids'] = []
668 extras['txn_id'] = txn_id
667 extras['txn_id'] = txn_id
669 extras['new_refs'] = {
668 extras['new_refs'] = {
670 'total_commits': 1,
669 'total_commits': 1,
671 'branches': branches,
670 'branches': branches,
672 'bookmarks': [],
671 'bookmarks': [],
673 'tags': tags,
672 'tags': tags,
674 }
673 }
675
674
676 return _call_hook('pre_push', extras, SvnMessageWriter())
675 return _call_hook('pre_push', extras, SvnMessageWriter())
677
676
678
677
679 def svn_post_commit(repo_path, commit_data, env):
678 def svn_post_commit(repo_path, commit_data, env):
680 """
679 """
681 commit_data is path, rev, txn_id
680 commit_data is path, rev, txn_id
682 """
681 """
683 path, commit_id, txn_id = commit_data
682 path, commit_id, txn_id = commit_data
684 branches = []
683 branches = []
685 tags = []
684 tags = []
686
685
687 if env.get('RC_SCM_DATA'):
686 if env.get('RC_SCM_DATA'):
688 extras = json.loads(env['RC_SCM_DATA'])
687 extras = json.loads(env['RC_SCM_DATA'])
689 else:
688 else:
690 # fallback method to read from TXN-ID stored data
689 # fallback method to read from TXN-ID stored data
691 extras = _get_extras_from_commit_id(commit_id, path)
690 extras = _get_extras_from_commit_id(commit_id, path)
692 if not extras:
691 if not extras:
693 return 0
692 return 0
694
693
695 extras['hook_type'] = 'post_commit'
694 extras['hook_type'] = 'post_commit'
696 extras['commit_ids'] = [commit_id]
695 extras['commit_ids'] = [commit_id]
697 extras['txn_id'] = txn_id
696 extras['txn_id'] = txn_id
698 extras['new_refs'] = {
697 extras['new_refs'] = {
699 'branches': branches,
698 'branches': branches,
700 'bookmarks': [],
699 'bookmarks': [],
701 'tags': tags,
700 'tags': tags,
702 'total_commits': 1,
701 'total_commits': 1,
703 }
702 }
704
703
705 if 'repo_size' in extras['hooks']:
704 if 'repo_size' in extras['hooks']:
706 try:
705 try:
707 _call_hook('repo_size', extras, SvnMessageWriter())
706 _call_hook('repo_size', extras, SvnMessageWriter())
708 except Exception:
707 except Exception:
709 pass
708 pass
710
709
711 return _call_hook('post_push', extras, SvnMessageWriter())
710 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,607 +1,610 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import base64
20 import base64
21 import locale
21 import locale
22 import logging
22 import logging
23 import uuid
23 import uuid
24 import wsgiref.util
24 import wsgiref.util
25 import traceback
25 import traceback
26 import tempfile
26 import tempfile
27 from itertools import chain
27 from itertools import chain
28
28
29 import simplejson as json
29 import simplejson as json
30 import msgpack
30 import msgpack
31 from pyramid.config import Configurator
31 from pyramid.config import Configurator
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.compat import configparser
34 from pyramid.compat import configparser
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
39 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
40 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
41
41
42 try:
42 try:
43 locale.setlocale(locale.LC_ALL, '')
43 locale.setlocale(locale.LC_ALL, '')
44 except locale.Error as e:
44 except locale.Error as e:
45 log.error(
45 log.error(
46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
46 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
47 os.environ['LC_ALL'] = 'C'
47 os.environ['LC_ALL'] = 'C'
48
48
49 import vcsserver
49 import vcsserver
50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
50 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
51 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
52 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
53 from vcsserver.echo_stub.echo_app import EchoApp
53 from vcsserver.echo_stub.echo_app import EchoApp
54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
54 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
55 from vcsserver.lib.exc_tracking import store_exception
55 from vcsserver.lib.exc_tracking import store_exception
56 from vcsserver.server import VcsServer
56 from vcsserver.server import VcsServer
57
57
58 try:
58 try:
59 from vcsserver.git import GitFactory, GitRemote
59 from vcsserver.git import GitFactory, GitRemote
60 except ImportError:
60 except ImportError:
61 GitFactory = None
61 GitFactory = None
62 GitRemote = None
62 GitRemote = None
63
63
64 try:
64 try:
65 from vcsserver.hg import MercurialFactory, HgRemote
65 from vcsserver.hg import MercurialFactory, HgRemote
66 except ImportError:
66 except ImportError:
67 MercurialFactory = None
67 MercurialFactory = None
68 HgRemote = None
68 HgRemote = None
69
69
70 try:
70 try:
71 from vcsserver.svn import SubversionFactory, SvnRemote
71 from vcsserver.svn import SubversionFactory, SvnRemote
72 except ImportError:
72 except ImportError:
73 SubversionFactory = None
73 SubversionFactory = None
74 SvnRemote = None
74 SvnRemote = None
75
75
76
76
77 def _is_request_chunked(environ):
77 def _is_request_chunked(environ):
78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
78 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
79 return stream
79 return stream
80
80
81
81
82 def _int_setting(settings, name, default):
82 def _int_setting(settings, name, default):
83 settings[name] = int(settings.get(name, default))
83 settings[name] = int(settings.get(name, default))
84 return settings[name]
84 return settings[name]
85
85
86
86
87 def _bool_setting(settings, name, default):
87 def _bool_setting(settings, name, default):
88 input_val = settings.get(name, default)
88 input_val = settings.get(name, default)
89 if isinstance(input_val, unicode):
89 if isinstance(input_val, unicode):
90 input_val = input_val.encode('utf8')
90 input_val = input_val.encode('utf8')
91 settings[name] = asbool(input_val)
91 settings[name] = asbool(input_val)
92 return settings[name]
92 return settings[name]
93
93
94
94
95 def _list_setting(settings, name, default):
95 def _list_setting(settings, name, default):
96 raw_value = settings.get(name, default)
96 raw_value = settings.get(name, default)
97
97
98 # Otherwise we assume it uses pyramids space/newline separation.
98 # Otherwise we assume it uses pyramids space/newline separation.
99 settings[name] = aslist(raw_value)
99 settings[name] = aslist(raw_value)
100 return settings[name]
100 return settings[name]
101
101
102
102
103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
103 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
104 value = settings.get(name, default)
104 value = settings.get(name, default)
105
105
106 if default_when_empty and not value:
106 if default_when_empty and not value:
107 # use default value when value is empty
107 # use default value when value is empty
108 value = default
108 value = default
109
109
110 if lower:
110 if lower:
111 value = value.lower()
111 value = value.lower()
112 settings[name] = value
112 settings[name] = value
113 return settings[name]
113 return settings[name]
114
114
115
115
116 class VCS(object):
116 class VCS(object):
117 def __init__(self, locale=None, cache_config=None):
117 def __init__(self, locale=None, cache_config=None):
118 self.locale = locale
118 self.locale = locale
119 self.cache_config = cache_config
119 self.cache_config = cache_config
120 self._configure_locale()
120 self._configure_locale()
121
121
122 if GitFactory and GitRemote:
122 if GitFactory and GitRemote:
123 git_factory = GitFactory()
123 git_factory = GitFactory()
124 self._git_remote = GitRemote(git_factory)
124 self._git_remote = GitRemote(git_factory)
125 else:
125 else:
126 log.info("Git client import failed")
126 log.info("Git client import failed")
127
127
128 if MercurialFactory and HgRemote:
128 if MercurialFactory and HgRemote:
129 hg_factory = MercurialFactory()
129 hg_factory = MercurialFactory()
130 self._hg_remote = HgRemote(hg_factory)
130 self._hg_remote = HgRemote(hg_factory)
131 else:
131 else:
132 log.info("Mercurial client import failed")
132 log.info("Mercurial client import failed")
133
133
134 if SubversionFactory and SvnRemote:
134 if SubversionFactory and SvnRemote:
135 svn_factory = SubversionFactory()
135 svn_factory = SubversionFactory()
136
136
137 # hg factory is used for svn url validation
137 # hg factory is used for svn url validation
138 hg_factory = MercurialFactory()
138 hg_factory = MercurialFactory()
139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
139 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
140 else:
140 else:
141 log.info("Subversion client import failed")
141 log.info("Subversion client import failed")
142
142
143 self._vcsserver = VcsServer()
143 self._vcsserver = VcsServer()
144
144
145 def _configure_locale(self):
145 def _configure_locale(self):
146 if self.locale:
146 if self.locale:
147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
147 log.info('Settings locale: `LC_ALL` to %s', self.locale)
148 else:
148 else:
149 log.info(
149 log.info(
150 'Configuring locale subsystem based on environment variables')
150 'Configuring locale subsystem based on environment variables')
151 try:
151 try:
152 # If self.locale is the empty string, then the locale
152 # If self.locale is the empty string, then the locale
153 # module will use the environment variables. See the
153 # module will use the environment variables. See the
154 # documentation of the package `locale`.
154 # documentation of the package `locale`.
155 locale.setlocale(locale.LC_ALL, self.locale)
155 locale.setlocale(locale.LC_ALL, self.locale)
156
156
157 language_code, encoding = locale.getlocale()
157 language_code, encoding = locale.getlocale()
158 log.info(
158 log.info(
159 'Locale set to language code "%s" with encoding "%s".',
159 'Locale set to language code "%s" with encoding "%s".',
160 language_code, encoding)
160 language_code, encoding)
161 except locale.Error:
161 except locale.Error:
162 log.exception(
162 log.exception(
163 'Cannot set locale, not configuring the locale system')
163 'Cannot set locale, not configuring the locale system')
164
164
165
165
166 class WsgiProxy(object):
166 class WsgiProxy(object):
167 def __init__(self, wsgi):
167 def __init__(self, wsgi):
168 self.wsgi = wsgi
168 self.wsgi = wsgi
169
169
170 def __call__(self, environ, start_response):
170 def __call__(self, environ, start_response):
171 input_data = environ['wsgi.input'].read()
171 input_data = environ['wsgi.input'].read()
172 input_data = msgpack.unpackb(input_data)
172 input_data = msgpack.unpackb(input_data)
173
173
174 error = None
174 error = None
175 try:
175 try:
176 data, status, headers = self.wsgi.handle(
176 data, status, headers = self.wsgi.handle(
177 input_data['environment'], input_data['input_data'],
177 input_data['environment'], input_data['input_data'],
178 *input_data['args'], **input_data['kwargs'])
178 *input_data['args'], **input_data['kwargs'])
179 except Exception as e:
179 except Exception as e:
180 data, status, headers = [], None, None
180 data, status, headers = [], None, None
181 error = {
181 error = {
182 'message': str(e),
182 'message': str(e),
183 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 '_vcs_kind': getattr(e, '_vcs_kind', None)
184 }
184 }
185
185
186 start_response(200, {})
186 start_response(200, {})
187 return self._iterator(error, status, headers, data)
187 return self._iterator(error, status, headers, data)
188
188
189 def _iterator(self, error, status, headers, data):
189 def _iterator(self, error, status, headers, data):
190 initial_data = [
190 initial_data = [
191 error,
191 error,
192 status,
192 status,
193 headers,
193 headers,
194 ]
194 ]
195
195
196 for d in chain(initial_data, data):
196 for d in chain(initial_data, data):
197 yield msgpack.packb(d)
197 yield msgpack.packb(d)
198
198
199
199
200 def not_found(request):
200 def not_found(request):
201 return {'status': '404 NOT FOUND'}
201 return {'status': '404 NOT FOUND'}
202
202
203
203
204 class VCSViewPredicate(object):
204 class VCSViewPredicate(object):
205 def __init__(self, val, config):
205 def __init__(self, val, config):
206 self.remotes = val
206 self.remotes = val
207
207
208 def text(self):
208 def text(self):
209 return 'vcs view method = %s' % (self.remotes.keys(),)
209 return 'vcs view method = %s' % (self.remotes.keys(),)
210
210
211 phash = text
211 phash = text
212
212
213 def __call__(self, context, request):
213 def __call__(self, context, request):
214 """
214 """
215 View predicate that returns true if given backend is supported by
215 View predicate that returns true if given backend is supported by
216 defined remotes.
216 defined remotes.
217 """
217 """
218 backend = request.matchdict.get('backend')
218 backend = request.matchdict.get('backend')
219 return backend in self.remotes
219 return backend in self.remotes
220
220
221
221
222 class HTTPApplication(object):
222 class HTTPApplication(object):
223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
224
224
225 remote_wsgi = remote_wsgi
225 remote_wsgi = remote_wsgi
226 _use_echo_app = False
226 _use_echo_app = False
227
227
228 def __init__(self, settings=None, global_config=None):
228 def __init__(self, settings=None, global_config=None):
229 self._sanitize_settings_and_apply_defaults(settings)
229 self._sanitize_settings_and_apply_defaults(settings)
230
230
231 self.config = Configurator(settings=settings)
231 self.config = Configurator(settings=settings)
232 self.global_config = global_config
232 self.global_config = global_config
233 self.config.include('vcsserver.lib.rc_cache')
233 self.config.include('vcsserver.lib.rc_cache')
234
234
235 locale = settings.get('locale', '') or 'en_US.UTF-8'
235 locale = settings.get('locale', '') or 'en_US.UTF-8'
236 vcs = VCS(locale=locale, cache_config=settings)
236 vcs = VCS(locale=locale, cache_config=settings)
237 self._remotes = {
237 self._remotes = {
238 'hg': vcs._hg_remote,
238 'hg': vcs._hg_remote,
239 'git': vcs._git_remote,
239 'git': vcs._git_remote,
240 'svn': vcs._svn_remote,
240 'svn': vcs._svn_remote,
241 'server': vcs._vcsserver,
241 'server': vcs._vcsserver,
242 }
242 }
243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
243 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
244 self._use_echo_app = True
244 self._use_echo_app = True
245 log.warning("Using EchoApp for VCS operations.")
245 log.warning("Using EchoApp for VCS operations.")
246 self.remote_wsgi = remote_wsgi_stub
246 self.remote_wsgi = remote_wsgi_stub
247
247
248 self._configure_settings(global_config, settings)
248 self._configure_settings(global_config, settings)
249 self._configure()
249 self._configure()
250
250
251 def _configure_settings(self, global_config, app_settings):
251 def _configure_settings(self, global_config, app_settings):
252 """
252 """
253 Configure the settings module.
253 Configure the settings module.
254 """
254 """
255 settings_merged = global_config.copy()
255 settings_merged = global_config.copy()
256 settings_merged.update(app_settings)
256 settings_merged.update(app_settings)
257
257
258 git_path = app_settings.get('git_path', None)
258 git_path = app_settings.get('git_path', None)
259 if git_path:
259 if git_path:
260 settings.GIT_EXECUTABLE = git_path
260 settings.GIT_EXECUTABLE = git_path
261 binary_dir = app_settings.get('core.binary_dir', None)
261 binary_dir = app_settings.get('core.binary_dir', None)
262 if binary_dir:
262 if binary_dir:
263 settings.BINARY_DIR = binary_dir
263 settings.BINARY_DIR = binary_dir
264
264
265 # Store the settings to make them available to other modules.
265 # Store the settings to make them available to other modules.
266 vcsserver.PYRAMID_SETTINGS = settings_merged
266 vcsserver.PYRAMID_SETTINGS = settings_merged
267 vcsserver.CONFIG = settings_merged
267 vcsserver.CONFIG = settings_merged
268
268
269 def _sanitize_settings_and_apply_defaults(self, settings):
269 def _sanitize_settings_and_apply_defaults(self, settings):
270 temp_store = tempfile.gettempdir()
270 temp_store = tempfile.gettempdir()
271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
271 default_cache_dir = os.path.join(temp_store, 'rc_cache')
272
272
273 # save default, cache dir, and use it for all backends later.
273 # save default, cache dir, and use it for all backends later.
274 default_cache_dir = _string_setting(
274 default_cache_dir = _string_setting(
275 settings,
275 settings,
276 'cache_dir',
276 'cache_dir',
277 default_cache_dir, lower=False, default_when_empty=True)
277 default_cache_dir, lower=False, default_when_empty=True)
278
278
279 # ensure we have our dir created
279 # ensure we have our dir created
280 if not os.path.isdir(default_cache_dir):
280 if not os.path.isdir(default_cache_dir):
281 os.makedirs(default_cache_dir, mode=0o755)
281 os.makedirs(default_cache_dir, mode=0o755)
282
282
283 # exception store cache
283 # exception store cache
284 _string_setting(
284 _string_setting(
285 settings,
285 settings,
286 'exception_tracker.store_path',
286 'exception_tracker.store_path',
287 temp_store, lower=False, default_when_empty=True)
287 temp_store, lower=False, default_when_empty=True)
288
288
289 # repo_object cache
289 # repo_object cache
290 _string_setting(
290 _string_setting(
291 settings,
291 settings,
292 'rc_cache.repo_object.backend',
292 'rc_cache.repo_object.backend',
293 'dogpile.cache.rc.memory_lru')
293 'dogpile.cache.rc.memory_lru')
294 _int_setting(
294 _int_setting(
295 settings,
295 settings,
296 'rc_cache.repo_object.expiration_time',
296 'rc_cache.repo_object.expiration_time',
297 300)
297 300)
298 _int_setting(
298 _int_setting(
299 settings,
299 settings,
300 'rc_cache.repo_object.max_size',
300 'rc_cache.repo_object.max_size',
301 1024)
301 1024)
302
302
303 def _configure(self):
303 def _configure(self):
304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
304 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
305
305
306 self.config.add_route('service', '/_service')
306 self.config.add_route('service', '/_service')
307 self.config.add_route('status', '/status')
307 self.config.add_route('status', '/status')
308 self.config.add_route('hg_proxy', '/proxy/hg')
308 self.config.add_route('hg_proxy', '/proxy/hg')
309 self.config.add_route('git_proxy', '/proxy/git')
309 self.config.add_route('git_proxy', '/proxy/git')
310 self.config.add_route('vcs', '/{backend}')
310 self.config.add_route('vcs', '/{backend}')
311 self.config.add_route('stream_git', '/stream/git/*repo_name')
311 self.config.add_route('stream_git', '/stream/git/*repo_name')
312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
312 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
313
313
314 self.config.add_view(self.status_view, route_name='status', renderer='json')
314 self.config.add_view(self.status_view, route_name='status', renderer='json')
315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
315 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
316
316
317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
317 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
318 self.config.add_view(self.git_proxy(), route_name='git_proxy')
319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
319 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
320 vcs_view=self._remotes)
320 vcs_view=self._remotes)
321
321
322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
322 self.config.add_view(self.hg_stream(), route_name='stream_hg')
323 self.config.add_view(self.git_stream(), route_name='stream_git')
323 self.config.add_view(self.git_stream(), route_name='stream_git')
324
324
325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
325 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
326
326
327 self.config.add_notfound_view(not_found, renderer='json')
327 self.config.add_notfound_view(not_found, renderer='json')
328
328
329 self.config.add_view(self.handle_vcs_exception, context=Exception)
329 self.config.add_view(self.handle_vcs_exception, context=Exception)
330
330
331 self.config.add_tween(
331 self.config.add_tween(
332 'vcsserver.tweens.RequestWrapperTween',
332 'vcsserver.tweens.RequestWrapperTween',
333 )
333 )
334
334
335 def wsgi_app(self):
335 def wsgi_app(self):
336 return self.config.make_wsgi_app()
336 return self.config.make_wsgi_app()
337
337
338 def vcs_view(self, request):
338 def vcs_view(self, request):
339 remote = self._remotes[request.matchdict['backend']]
339 remote = self._remotes[request.matchdict['backend']]
340 payload = msgpack.unpackb(request.body, use_list=True)
340 payload = msgpack.unpackb(request.body, use_list=True)
341 method = payload.get('method')
341 method = payload.get('method')
342 params = payload.get('params')
342 params = payload.get('params')
343 wire = params.get('wire')
343 wire = params.get('wire')
344 args = params.get('args')
344 args = params.get('args')
345 kwargs = params.get('kwargs')
345 kwargs = params.get('kwargs')
346 context_uid = None
346 context_uid = None
347
347
348 if wire:
348 if wire:
349 try:
349 try:
350 wire['context'] = context_uid = uuid.UUID(wire['context'])
350 wire['context'] = context_uid = uuid.UUID(wire['context'])
351 except KeyError:
351 except KeyError:
352 pass
352 pass
353 args.insert(0, wire)
353 args.insert(0, wire)
354
354
355 log.debug('method called:%s with kwargs:%s context_uid: %s',
355 log.debug('method called:%s with kwargs:%s context_uid: %s',
356 method, kwargs, context_uid)
356 method, kwargs, context_uid)
357 try:
357 try:
358 resp = getattr(remote, method)(*args, **kwargs)
358 resp = getattr(remote, method)(*args, **kwargs)
359 except Exception as e:
359 except Exception as e:
360 exc_info = list(sys.exc_info())
360 exc_info = list(sys.exc_info())
361 exc_type, exc_value, exc_traceback = exc_info
361 exc_type, exc_value, exc_traceback = exc_info
362
362
363 org_exc = getattr(e, '_org_exc', None)
363 org_exc = getattr(e, '_org_exc', None)
364 org_exc_name = None
364 org_exc_name = None
365 org_exc_tb = ''
365 if org_exc:
366 if org_exc:
366 org_exc_name = org_exc.__class__.__name__
367 org_exc_name = org_exc.__class__.__name__
368 org_exc_tb = getattr(e, '_org_exc_tb', '')
367 # replace our "faked" exception with our org
369 # replace our "faked" exception with our org
368 exc_info[0] = org_exc.__class__
370 exc_info[0] = org_exc.__class__
369 exc_info[1] = org_exc
371 exc_info[1] = org_exc
370
372
371 store_exception(id(exc_info), exc_info)
373 store_exception(id(exc_info), exc_info)
372
374
373 tb_info = ''.join(
375 tb_info = ''.join(
374 traceback.format_exception(exc_type, exc_value, exc_traceback))
376 traceback.format_exception(exc_type, exc_value, exc_traceback))
375
377
376 type_ = e.__class__.__name__
378 type_ = e.__class__.__name__
377 if type_ not in self.ALLOWED_EXCEPTIONS:
379 if type_ not in self.ALLOWED_EXCEPTIONS:
378 type_ = None
380 type_ = None
379
381
380 resp = {
382 resp = {
381 'id': payload.get('id'),
383 'id': payload.get('id'),
382 'error': {
384 'error': {
383 'message': e.message,
385 'message': e.message,
384 'traceback': tb_info,
386 'traceback': tb_info,
385 'org_exc': org_exc_name,
387 'org_exc': org_exc_name,
388 'org_exc_tb': org_exc_tb,
386 'type': type_
389 'type': type_
387 }
390 }
388 }
391 }
389 try:
392 try:
390 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
393 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
391 except AttributeError:
394 except AttributeError:
392 pass
395 pass
393 else:
396 else:
394 resp = {
397 resp = {
395 'id': payload.get('id'),
398 'id': payload.get('id'),
396 'result': resp
399 'result': resp
397 }
400 }
398
401
399 return resp
402 return resp
400
403
401 def status_view(self, request):
404 def status_view(self, request):
402 import vcsserver
405 import vcsserver
403 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
406 return {'status': 'OK', 'vcsserver_version': vcsserver.__version__,
404 'pid': os.getpid()}
407 'pid': os.getpid()}
405
408
406 def service_view(self, request):
409 def service_view(self, request):
407 import vcsserver
410 import vcsserver
408
411
409 payload = msgpack.unpackb(request.body, use_list=True)
412 payload = msgpack.unpackb(request.body, use_list=True)
410
413
411 try:
414 try:
412 path = self.global_config['__file__']
415 path = self.global_config['__file__']
413 config = configparser.ConfigParser()
416 config = configparser.ConfigParser()
414 config.read(path)
417 config.read(path)
415 parsed_ini = config
418 parsed_ini = config
416 if parsed_ini.has_section('server:main'):
419 if parsed_ini.has_section('server:main'):
417 parsed_ini = dict(parsed_ini.items('server:main'))
420 parsed_ini = dict(parsed_ini.items('server:main'))
418 except Exception:
421 except Exception:
419 log.exception('Failed to read .ini file for display')
422 log.exception('Failed to read .ini file for display')
420 parsed_ini = {}
423 parsed_ini = {}
421
424
422 resp = {
425 resp = {
423 'id': payload.get('id'),
426 'id': payload.get('id'),
424 'result': dict(
427 'result': dict(
425 version=vcsserver.__version__,
428 version=vcsserver.__version__,
426 config=parsed_ini,
429 config=parsed_ini,
427 payload=payload,
430 payload=payload,
428 )
431 )
429 }
432 }
430 return resp
433 return resp
431
434
432 def _msgpack_renderer_factory(self, info):
435 def _msgpack_renderer_factory(self, info):
433 def _render(value, system):
436 def _render(value, system):
434 value = msgpack.packb(value)
437 value = msgpack.packb(value)
435 request = system.get('request')
438 request = system.get('request')
436 if request is not None:
439 if request is not None:
437 response = request.response
440 response = request.response
438 ct = response.content_type
441 ct = response.content_type
439 if ct == response.default_content_type:
442 if ct == response.default_content_type:
440 response.content_type = 'application/x-msgpack'
443 response.content_type = 'application/x-msgpack'
441 return value
444 return value
442 return _render
445 return _render
443
446
444 def set_env_from_config(self, environ, config):
447 def set_env_from_config(self, environ, config):
445 dict_conf = {}
448 dict_conf = {}
446 try:
449 try:
447 for elem in config:
450 for elem in config:
448 if elem[0] == 'rhodecode':
451 if elem[0] == 'rhodecode':
449 dict_conf = json.loads(elem[2])
452 dict_conf = json.loads(elem[2])
450 break
453 break
451 except Exception:
454 except Exception:
452 log.exception('Failed to fetch SCM CONFIG')
455 log.exception('Failed to fetch SCM CONFIG')
453 return
456 return
454
457
455 username = dict_conf.get('username')
458 username = dict_conf.get('username')
456 if username:
459 if username:
457 environ['REMOTE_USER'] = username
460 environ['REMOTE_USER'] = username
458 # mercurial specific, some extension api rely on this
461 # mercurial specific, some extension api rely on this
459 environ['HGUSER'] = username
462 environ['HGUSER'] = username
460
463
461 ip = dict_conf.get('ip')
464 ip = dict_conf.get('ip')
462 if ip:
465 if ip:
463 environ['REMOTE_HOST'] = ip
466 environ['REMOTE_HOST'] = ip
464
467
465 if _is_request_chunked(environ):
468 if _is_request_chunked(environ):
466 # set the compatibility flag for webob
469 # set the compatibility flag for webob
467 environ['wsgi.input_terminated'] = True
470 environ['wsgi.input_terminated'] = True
468
471
469 def hg_proxy(self):
472 def hg_proxy(self):
470 @wsgiapp
473 @wsgiapp
471 def _hg_proxy(environ, start_response):
474 def _hg_proxy(environ, start_response):
472 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
475 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
473 return app(environ, start_response)
476 return app(environ, start_response)
474 return _hg_proxy
477 return _hg_proxy
475
478
476 def git_proxy(self):
479 def git_proxy(self):
477 @wsgiapp
480 @wsgiapp
478 def _git_proxy(environ, start_response):
481 def _git_proxy(environ, start_response):
479 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
482 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
480 return app(environ, start_response)
483 return app(environ, start_response)
481 return _git_proxy
484 return _git_proxy
482
485
483 def hg_stream(self):
486 def hg_stream(self):
484 if self._use_echo_app:
487 if self._use_echo_app:
485 @wsgiapp
488 @wsgiapp
486 def _hg_stream(environ, start_response):
489 def _hg_stream(environ, start_response):
487 app = EchoApp('fake_path', 'fake_name', None)
490 app = EchoApp('fake_path', 'fake_name', None)
488 return app(environ, start_response)
491 return app(environ, start_response)
489 return _hg_stream
492 return _hg_stream
490 else:
493 else:
491 @wsgiapp
494 @wsgiapp
492 def _hg_stream(environ, start_response):
495 def _hg_stream(environ, start_response):
493 log.debug('http-app: handling hg stream')
496 log.debug('http-app: handling hg stream')
494 repo_path = environ['HTTP_X_RC_REPO_PATH']
497 repo_path = environ['HTTP_X_RC_REPO_PATH']
495 repo_name = environ['HTTP_X_RC_REPO_NAME']
498 repo_name = environ['HTTP_X_RC_REPO_NAME']
496 packed_config = base64.b64decode(
499 packed_config = base64.b64decode(
497 environ['HTTP_X_RC_REPO_CONFIG'])
500 environ['HTTP_X_RC_REPO_CONFIG'])
498 config = msgpack.unpackb(packed_config)
501 config = msgpack.unpackb(packed_config)
499 app = scm_app.create_hg_wsgi_app(
502 app = scm_app.create_hg_wsgi_app(
500 repo_path, repo_name, config)
503 repo_path, repo_name, config)
501
504
502 # Consistent path information for hgweb
505 # Consistent path information for hgweb
503 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
506 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
504 environ['REPO_NAME'] = repo_name
507 environ['REPO_NAME'] = repo_name
505 self.set_env_from_config(environ, config)
508 self.set_env_from_config(environ, config)
506
509
507 log.debug('http-app: starting app handler '
510 log.debug('http-app: starting app handler '
508 'with %s and process request', app)
511 'with %s and process request', app)
509 return app(environ, ResponseFilter(start_response))
512 return app(environ, ResponseFilter(start_response))
510 return _hg_stream
513 return _hg_stream
511
514
512 def git_stream(self):
515 def git_stream(self):
513 if self._use_echo_app:
516 if self._use_echo_app:
514 @wsgiapp
517 @wsgiapp
515 def _git_stream(environ, start_response):
518 def _git_stream(environ, start_response):
516 app = EchoApp('fake_path', 'fake_name', None)
519 app = EchoApp('fake_path', 'fake_name', None)
517 return app(environ, start_response)
520 return app(environ, start_response)
518 return _git_stream
521 return _git_stream
519 else:
522 else:
520 @wsgiapp
523 @wsgiapp
521 def _git_stream(environ, start_response):
524 def _git_stream(environ, start_response):
522 log.debug('http-app: handling git stream')
525 log.debug('http-app: handling git stream')
523 repo_path = environ['HTTP_X_RC_REPO_PATH']
526 repo_path = environ['HTTP_X_RC_REPO_PATH']
524 repo_name = environ['HTTP_X_RC_REPO_NAME']
527 repo_name = environ['HTTP_X_RC_REPO_NAME']
525 packed_config = base64.b64decode(
528 packed_config = base64.b64decode(
526 environ['HTTP_X_RC_REPO_CONFIG'])
529 environ['HTTP_X_RC_REPO_CONFIG'])
527 config = msgpack.unpackb(packed_config)
530 config = msgpack.unpackb(packed_config)
528
531
529 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
532 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
530 self.set_env_from_config(environ, config)
533 self.set_env_from_config(environ, config)
531
534
532 content_type = environ.get('CONTENT_TYPE', '')
535 content_type = environ.get('CONTENT_TYPE', '')
533
536
534 path = environ['PATH_INFO']
537 path = environ['PATH_INFO']
535 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
538 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
536 log.debug(
539 log.debug(
537 'LFS: Detecting if request `%s` is LFS server path based '
540 'LFS: Detecting if request `%s` is LFS server path based '
538 'on content type:`%s`, is_lfs:%s',
541 'on content type:`%s`, is_lfs:%s',
539 path, content_type, is_lfs_request)
542 path, content_type, is_lfs_request)
540
543
541 if not is_lfs_request:
544 if not is_lfs_request:
542 # fallback detection by path
545 # fallback detection by path
543 if GIT_LFS_PROTO_PAT.match(path):
546 if GIT_LFS_PROTO_PAT.match(path):
544 is_lfs_request = True
547 is_lfs_request = True
545 log.debug(
548 log.debug(
546 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
549 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
547 path, is_lfs_request)
550 path, is_lfs_request)
548
551
549 if is_lfs_request:
552 if is_lfs_request:
550 app = scm_app.create_git_lfs_wsgi_app(
553 app = scm_app.create_git_lfs_wsgi_app(
551 repo_path, repo_name, config)
554 repo_path, repo_name, config)
552 else:
555 else:
553 app = scm_app.create_git_wsgi_app(
556 app = scm_app.create_git_wsgi_app(
554 repo_path, repo_name, config)
557 repo_path, repo_name, config)
555
558
556 log.debug('http-app: starting app handler '
559 log.debug('http-app: starting app handler '
557 'with %s and process request', app)
560 'with %s and process request', app)
558
561
559 return app(environ, start_response)
562 return app(environ, start_response)
560
563
561 return _git_stream
564 return _git_stream
562
565
563 def handle_vcs_exception(self, exception, request):
566 def handle_vcs_exception(self, exception, request):
564 _vcs_kind = getattr(exception, '_vcs_kind', '')
567 _vcs_kind = getattr(exception, '_vcs_kind', '')
565 if _vcs_kind == 'repo_locked':
568 if _vcs_kind == 'repo_locked':
566 # Get custom repo-locked status code if present.
569 # Get custom repo-locked status code if present.
567 status_code = request.headers.get('X-RC-Locked-Status-Code')
570 status_code = request.headers.get('X-RC-Locked-Status-Code')
568 return HTTPRepoLocked(
571 return HTTPRepoLocked(
569 title=exception.message, status_code=status_code)
572 title=exception.message, status_code=status_code)
570
573
571 elif _vcs_kind == 'repo_branch_protected':
574 elif _vcs_kind == 'repo_branch_protected':
572 # Get custom repo-branch-protected status code if present.
575 # Get custom repo-branch-protected status code if present.
573 return HTTPRepoBranchProtected(title=exception.message)
576 return HTTPRepoBranchProtected(title=exception.message)
574
577
575 exc_info = request.exc_info
578 exc_info = request.exc_info
576 store_exception(id(exc_info), exc_info)
579 store_exception(id(exc_info), exc_info)
577
580
578 traceback_info = 'unavailable'
581 traceback_info = 'unavailable'
579 if request.exc_info:
582 if request.exc_info:
580 exc_type, exc_value, exc_tb = request.exc_info
583 exc_type, exc_value, exc_tb = request.exc_info
581 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
584 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
582
585
583 log.error(
586 log.error(
584 'error occurred handling this request for path: %s, \n tb: %s',
587 'error occurred handling this request for path: %s, \n tb: %s',
585 request.path, traceback_info)
588 request.path, traceback_info)
586 raise exception
589 raise exception
587
590
588
591
589 class ResponseFilter(object):
592 class ResponseFilter(object):
590
593
591 def __init__(self, start_response):
594 def __init__(self, start_response):
592 self._start_response = start_response
595 self._start_response = start_response
593
596
594 def __call__(self, status, response_headers, exc_info=None):
597 def __call__(self, status, response_headers, exc_info=None):
595 headers = tuple(
598 headers = tuple(
596 (h, v) for h, v in response_headers
599 (h, v) for h, v in response_headers
597 if not wsgiref.util.is_hop_by_hop(h))
600 if not wsgiref.util.is_hop_by_hop(h))
598 return self._start_response(status, headers, exc_info)
601 return self._start_response(status, headers, exc_info)
599
602
600
603
601 def main(global_config, **settings):
604 def main(global_config, **settings):
602 if MercurialFactory:
605 if MercurialFactory:
603 hgpatches.patch_largefiles_capabilities()
606 hgpatches.patch_largefiles_capabilities()
604 hgpatches.patch_subrepo_type_mapping()
607 hgpatches.patch_subrepo_type_mapping()
605
608
606 app = HTTPApplication(settings=settings, global_config=global_config)
609 app = HTTPApplication(settings=settings, global_config=global_config)
607 return app.wsgi_app()
610 return app.wsgi_app()
@@ -1,16 +1,16 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,151 +1,151 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20
20
21 import os
21 import os
22 import time
22 import time
23 import datetime
23 import datetime
24 import msgpack
24 import msgpack
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import tempfile
27 import tempfile
28
28
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
32 # NOTE: Any changes should be synced with exc_tracking at rhodecode.lib.exc_tracking
33 global_prefix = 'vcsserver'
33 global_prefix = 'vcsserver'
34 exc_store_dir_name = 'rc_exception_store_v1'
34 exc_store_dir_name = 'rc_exception_store_v1'
35
35
36
36
37 def exc_serialize(exc_id, tb, exc_type):
37 def exc_serialize(exc_id, tb, exc_type):
38
38
39 data = {
39 data = {
40 'version': 'v1',
40 'version': 'v1',
41 'exc_id': exc_id,
41 'exc_id': exc_id,
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 'exc_timestamp': repr(time.time()),
43 'exc_timestamp': repr(time.time()),
44 'exc_message': tb,
44 'exc_message': tb,
45 'exc_type': exc_type,
45 'exc_type': exc_type,
46 }
46 }
47 return msgpack.packb(data), data
47 return msgpack.packb(data), data
48
48
49
49
50 def exc_unserialize(tb):
50 def exc_unserialize(tb):
51 return msgpack.unpackb(tb)
51 return msgpack.unpackb(tb)
52
52
53
53
54 def get_exc_store():
54 def get_exc_store():
55 """
55 """
56 Get and create exception store if it's not existing
56 Get and create exception store if it's not existing
57 """
57 """
58 import vcsserver as app
58 import vcsserver as app
59
59
60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
60 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
61 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
62
62
63 _exc_store_path = os.path.abspath(_exc_store_path)
63 _exc_store_path = os.path.abspath(_exc_store_path)
64 if not os.path.isdir(_exc_store_path):
64 if not os.path.isdir(_exc_store_path):
65 os.makedirs(_exc_store_path)
65 os.makedirs(_exc_store_path)
66 log.debug('Initializing exceptions store at %s', _exc_store_path)
66 log.debug('Initializing exceptions store at %s', _exc_store_path)
67 return _exc_store_path
67 return _exc_store_path
68
68
69
69
70 def _store_exception(exc_id, exc_info, prefix):
70 def _store_exception(exc_id, exc_info, prefix):
71 exc_type, exc_value, exc_traceback = exc_info
71 exc_type, exc_value, exc_traceback = exc_info
72 tb = ''.join(traceback.format_exception(
72 tb = ''.join(traceback.format_exception(
73 exc_type, exc_value, exc_traceback, None))
73 exc_type, exc_value, exc_traceback, None))
74
74
75 exc_type_name = exc_type.__name__
75 exc_type_name = exc_type.__name__
76 exc_store_path = get_exc_store()
76 exc_store_path = get_exc_store()
77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
77 exc_data, org_data = exc_serialize(exc_id, tb, exc_type_name)
78 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
78 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
79 if not os.path.isdir(exc_store_path):
79 if not os.path.isdir(exc_store_path):
80 os.makedirs(exc_store_path)
80 os.makedirs(exc_store_path)
81 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
81 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
82 with open(stored_exc_path, 'wb') as f:
82 with open(stored_exc_path, 'wb') as f:
83 f.write(exc_data)
83 f.write(exc_data)
84 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
84 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
85
85
86
86
87 def store_exception(exc_id, exc_info, prefix=global_prefix):
87 def store_exception(exc_id, exc_info, prefix=global_prefix):
88 """
88 """
89 Example usage::
89 Example usage::
90
90
91 exc_info = sys.exc_info()
91 exc_info = sys.exc_info()
92 store_exception(id(exc_info), exc_info)
92 store_exception(id(exc_info), exc_info)
93 """
93 """
94
94
95 try:
95 try:
96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
96 _store_exception(exc_id=exc_id, exc_info=exc_info, prefix=prefix)
97 except Exception:
97 except Exception:
98 log.exception('Failed to store exception `%s` information', exc_id)
98 log.exception('Failed to store exception `%s` information', exc_id)
99 # there's no way this can fail, it will crash server badly if it does.
99 # there's no way this can fail, it will crash server badly if it does.
100 pass
100 pass
101
101
102
102
103 def _find_exc_file(exc_id, prefix=global_prefix):
103 def _find_exc_file(exc_id, prefix=global_prefix):
104 exc_store_path = get_exc_store()
104 exc_store_path = get_exc_store()
105 if prefix:
105 if prefix:
106 exc_id = '{}_{}'.format(exc_id, prefix)
106 exc_id = '{}_{}'.format(exc_id, prefix)
107 else:
107 else:
108 # search without a prefix
108 # search without a prefix
109 exc_id = '{}'.format(exc_id)
109 exc_id = '{}'.format(exc_id)
110
110
111 # we need to search the store for such start pattern as above
111 # we need to search the store for such start pattern as above
112 for fname in os.listdir(exc_store_path):
112 for fname in os.listdir(exc_store_path):
113 if fname.startswith(exc_id):
113 if fname.startswith(exc_id):
114 exc_id = os.path.join(exc_store_path, fname)
114 exc_id = os.path.join(exc_store_path, fname)
115 break
115 break
116 continue
116 continue
117 else:
117 else:
118 exc_id = None
118 exc_id = None
119
119
120 return exc_id
120 return exc_id
121
121
122
122
123 def _read_exception(exc_id, prefix):
123 def _read_exception(exc_id, prefix):
124 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
124 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
125 if exc_id_file_path:
125 if exc_id_file_path:
126 with open(exc_id_file_path, 'rb') as f:
126 with open(exc_id_file_path, 'rb') as f:
127 return exc_unserialize(f.read())
127 return exc_unserialize(f.read())
128 else:
128 else:
129 log.debug('Exception File `%s` not found', exc_id_file_path)
129 log.debug('Exception File `%s` not found', exc_id_file_path)
130 return None
130 return None
131
131
132
132
133 def read_exception(exc_id, prefix=global_prefix):
133 def read_exception(exc_id, prefix=global_prefix):
134 try:
134 try:
135 return _read_exception(exc_id=exc_id, prefix=prefix)
135 return _read_exception(exc_id=exc_id, prefix=prefix)
136 except Exception:
136 except Exception:
137 log.exception('Failed to read exception `%s` information', exc_id)
137 log.exception('Failed to read exception `%s` information', exc_id)
138 # there's no way this can fail, it will crash server badly if it does.
138 # there's no way this can fail, it will crash server badly if it does.
139 return None
139 return None
140
140
141
141
142 def delete_exception(exc_id, prefix=global_prefix):
142 def delete_exception(exc_id, prefix=global_prefix):
143 try:
143 try:
144 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
144 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
145 if exc_id_file_path:
145 if exc_id_file_path:
146 os.remove(exc_id_file_path)
146 os.remove(exc_id_file_path)
147
147
148 except Exception:
148 except Exception:
149 log.exception('Failed to remove exception `%s` information', exc_id)
149 log.exception('Failed to remove exception `%s` information', exc_id)
150 # there's no way this can fail, it will crash server badly if it does.
150 # there's no way this can fail, it will crash server badly if it does.
151 pass
151 pass
@@ -1,65 +1,65 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20
20
21 import logging
21 import logging
22
22
23 from repoze.lru import LRUCache
23 from repoze.lru import LRUCache
24
24
25 from vcsserver.utils import safe_str
25 from vcsserver.utils import safe_str
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 class LRUDict(LRUCache):
30 class LRUDict(LRUCache):
31 """
31 """
32 Wrapper to provide partial dict access
32 Wrapper to provide partial dict access
33 """
33 """
34
34
35 def __setitem__(self, key, value):
35 def __setitem__(self, key, value):
36 return self.put(key, value)
36 return self.put(key, value)
37
37
38 def __getitem__(self, key):
38 def __getitem__(self, key):
39 return self.get(key)
39 return self.get(key)
40
40
41 def __contains__(self, key):
41 def __contains__(self, key):
42 return bool(self.get(key))
42 return bool(self.get(key))
43
43
44 def __delitem__(self, key):
44 def __delitem__(self, key):
45 del self.data[key]
45 del self.data[key]
46
46
47 def keys(self):
47 def keys(self):
48 return self.data.keys()
48 return self.data.keys()
49
49
50
50
51 class LRUDictDebug(LRUDict):
51 class LRUDictDebug(LRUDict):
52 """
52 """
53 Wrapper to provide some debug options
53 Wrapper to provide some debug options
54 """
54 """
55 def _report_keys(self):
55 def _report_keys(self):
56 elems_cnt = '%s/%s' % (len(self.keys()), self.size)
56 elems_cnt = '%s/%s' % (len(self.keys()), self.size)
57 # trick for pformat print it more nicely
57 # trick for pformat print it more nicely
58 fmt = '\n'
58 fmt = '\n'
59 for cnt, elem in enumerate(self.keys()):
59 for cnt, elem in enumerate(self.keys()):
60 fmt += '%s - %s\n' % (cnt+1, safe_str(elem))
60 fmt += '%s - %s\n' % (cnt+1, safe_str(elem))
61 log.debug('current LRU keys (%s):%s', elems_cnt, fmt)
61 log.debug('current LRU keys (%s):%s', elems_cnt, fmt)
62
62
63 def __getitem__(self, key):
63 def __getitem__(self, key):
64 self._report_keys()
64 self._report_keys()
65 return self.get(key)
65 return self.get(key)
@@ -1,60 +1,60 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import logging
18 import logging
19 from dogpile.cache import register_backend
19 from dogpile.cache import register_backend
20
20
21 register_backend(
21 register_backend(
22 "dogpile.cache.rc.memory_lru", "vcsserver.lib.rc_cache.backends",
22 "dogpile.cache.rc.memory_lru", "vcsserver.lib.rc_cache.backends",
23 "LRUMemoryBackend")
23 "LRUMemoryBackend")
24
24
25 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
26
26
27 from . import region_meta
27 from . import region_meta
28 from .util import key_generator, get_default_cache_settings, make_region
28 from .util import key_generator, get_default_cache_settings, make_region
29
29
30
30
31 def configure_dogpile_cache(settings):
31 def configure_dogpile_cache(settings):
32 cache_dir = settings.get('cache_dir')
32 cache_dir = settings.get('cache_dir')
33 if cache_dir:
33 if cache_dir:
34 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
34 region_meta.dogpile_config_defaults['cache_dir'] = cache_dir
35
35
36 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
36 rc_cache_data = get_default_cache_settings(settings, prefixes=['rc_cache.'])
37
37
38 # inspect available namespaces
38 # inspect available namespaces
39 avail_regions = set()
39 avail_regions = set()
40 for key in rc_cache_data.keys():
40 for key in rc_cache_data.keys():
41 namespace_name = key.split('.', 1)[0]
41 namespace_name = key.split('.', 1)[0]
42 avail_regions.add(namespace_name)
42 avail_regions.add(namespace_name)
43 log.debug('dogpile: found following cache regions: %s', avail_regions)
43 log.debug('dogpile: found following cache regions: %s', avail_regions)
44
44
45 # register them into namespace
45 # register them into namespace
46 for region_name in avail_regions:
46 for region_name in avail_regions:
47 new_region = make_region(
47 new_region = make_region(
48 name=region_name,
48 name=region_name,
49 function_key_generator=key_generator
49 function_key_generator=key_generator
50 )
50 )
51
51
52 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
52 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
53
53
54 log.debug('dogpile: registering a new region %s[%s]',
54 log.debug('dogpile: registering a new region %s[%s]',
55 region_name, new_region.__dict__)
55 region_name, new_region.__dict__)
56 region_meta.dogpile_cache_regions[region_name] = new_region
56 region_meta.dogpile_cache_regions[region_name] = new_region
57
57
58
58
59 def includeme(config):
59 def includeme(config):
60 configure_dogpile_cache(config.registry.settings)
60 configure_dogpile_cache(config.registry.settings)
@@ -1,51 +1,51 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import logging
18 import logging
19
19
20 from dogpile.cache.backends import memory as memory_backend
20 from dogpile.cache.backends import memory as memory_backend
21 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
21 from vcsserver.lib.memory_lru_dict import LRUDict, LRUDictDebug
22
22
23
23
24 _default_max_size = 1024
24 _default_max_size = 1024
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 class LRUMemoryBackend(memory_backend.MemoryBackend):
29 class LRUMemoryBackend(memory_backend.MemoryBackend):
30 pickle_values = False
30 pickle_values = False
31
31
32 def __init__(self, arguments):
32 def __init__(self, arguments):
33 max_size = arguments.pop('max_size', _default_max_size)
33 max_size = arguments.pop('max_size', _default_max_size)
34
34
35 LRUDictClass = LRUDict
35 LRUDictClass = LRUDict
36 if arguments.pop('log_key_count', None):
36 if arguments.pop('log_key_count', None):
37 LRUDictClass = LRUDictDebug
37 LRUDictClass = LRUDictDebug
38
38
39 arguments['cache_dict'] = LRUDictClass(max_size)
39 arguments['cache_dict'] = LRUDictClass(max_size)
40 super(LRUMemoryBackend, self).__init__(arguments)
40 super(LRUMemoryBackend, self).__init__(arguments)
41
41
42 def delete(self, key):
42 def delete(self, key):
43 try:
43 try:
44 del self._cache[key]
44 del self._cache[key]
45 except KeyError:
45 except KeyError:
46 # we don't care if key isn't there at deletion
46 # we don't care if key isn't there at deletion
47 pass
47 pass
48
48
49 def delete_multi(self, keys):
49 def delete_multi(self, keys):
50 for key in keys:
50 for key in keys:
51 self.delete(key)
51 self.delete(key)
@@ -1,26 +1,26 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import tempfile
19 import tempfile
20
20
21 dogpile_config_defaults = {
21 dogpile_config_defaults = {
22 'cache_dir': os.path.join(tempfile.gettempdir(), 'rc_cache')
22 'cache_dir': os.path.join(tempfile.gettempdir(), 'rc_cache')
23 }
23 }
24
24
25 # GLOBAL TO STORE ALL REGISTERED REGIONS
25 # GLOBAL TO STORE ALL REGISTERED REGIONS
26 dogpile_cache_regions = {}
26 dogpile_cache_regions = {}
@@ -1,136 +1,136 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import logging
19 import logging
20 import functools
20 import functools
21
21
22 from vcsserver.utils import safe_str, sha1
22 from vcsserver.utils import safe_str, sha1
23 from dogpile.cache import CacheRegion
23 from dogpile.cache import CacheRegion
24 from dogpile.cache.util import compat
24 from dogpile.cache.util import compat
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 class RhodeCodeCacheRegion(CacheRegion):
29 class RhodeCodeCacheRegion(CacheRegion):
30
30
31 def conditional_cache_on_arguments(
31 def conditional_cache_on_arguments(
32 self, namespace=None,
32 self, namespace=None,
33 expiration_time=None,
33 expiration_time=None,
34 should_cache_fn=None,
34 should_cache_fn=None,
35 to_str=compat.string_type,
35 to_str=compat.string_type,
36 function_key_generator=None,
36 function_key_generator=None,
37 condition=True):
37 condition=True):
38 """
38 """
39 Custom conditional decorator, that will not touch any dogpile internals if
39 Custom conditional decorator, that will not touch any dogpile internals if
40 condition isn't meet. This works a bit different than should_cache_fn
40 condition isn't meet. This works a bit different than should_cache_fn
41 And it's faster in cases we don't ever want to compute cached values
41 And it's faster in cases we don't ever want to compute cached values
42 """
42 """
43 expiration_time_is_callable = compat.callable(expiration_time)
43 expiration_time_is_callable = compat.callable(expiration_time)
44
44
45 if function_key_generator is None:
45 if function_key_generator is None:
46 function_key_generator = self.function_key_generator
46 function_key_generator = self.function_key_generator
47
47
48 def decorator(fn):
48 def decorator(fn):
49 if to_str is compat.string_type:
49 if to_str is compat.string_type:
50 # backwards compatible
50 # backwards compatible
51 key_generator = function_key_generator(namespace, fn)
51 key_generator = function_key_generator(namespace, fn)
52 else:
52 else:
53 key_generator = function_key_generator(namespace, fn, to_str=to_str)
53 key_generator = function_key_generator(namespace, fn, to_str=to_str)
54
54
55 @functools.wraps(fn)
55 @functools.wraps(fn)
56 def decorate(*arg, **kw):
56 def decorate(*arg, **kw):
57 key = key_generator(*arg, **kw)
57 key = key_generator(*arg, **kw)
58
58
59 @functools.wraps(fn)
59 @functools.wraps(fn)
60 def creator():
60 def creator():
61 return fn(*arg, **kw)
61 return fn(*arg, **kw)
62
62
63 if not condition:
63 if not condition:
64 return creator()
64 return creator()
65
65
66 timeout = expiration_time() if expiration_time_is_callable \
66 timeout = expiration_time() if expiration_time_is_callable \
67 else expiration_time
67 else expiration_time
68
68
69 return self.get_or_create(key, creator, timeout, should_cache_fn)
69 return self.get_or_create(key, creator, timeout, should_cache_fn)
70
70
71 def invalidate(*arg, **kw):
71 def invalidate(*arg, **kw):
72 key = key_generator(*arg, **kw)
72 key = key_generator(*arg, **kw)
73 self.delete(key)
73 self.delete(key)
74
74
75 def set_(value, *arg, **kw):
75 def set_(value, *arg, **kw):
76 key = key_generator(*arg, **kw)
76 key = key_generator(*arg, **kw)
77 self.set(key, value)
77 self.set(key, value)
78
78
79 def get(*arg, **kw):
79 def get(*arg, **kw):
80 key = key_generator(*arg, **kw)
80 key = key_generator(*arg, **kw)
81 return self.get(key)
81 return self.get(key)
82
82
83 def refresh(*arg, **kw):
83 def refresh(*arg, **kw):
84 key = key_generator(*arg, **kw)
84 key = key_generator(*arg, **kw)
85 value = fn(*arg, **kw)
85 value = fn(*arg, **kw)
86 self.set(key, value)
86 self.set(key, value)
87 return value
87 return value
88
88
89 decorate.set = set_
89 decorate.set = set_
90 decorate.invalidate = invalidate
90 decorate.invalidate = invalidate
91 decorate.refresh = refresh
91 decorate.refresh = refresh
92 decorate.get = get
92 decorate.get = get
93 decorate.original = fn
93 decorate.original = fn
94 decorate.key_generator = key_generator
94 decorate.key_generator = key_generator
95
95
96 return decorate
96 return decorate
97
97
98 return decorator
98 return decorator
99
99
100
100
101 def make_region(*arg, **kw):
101 def make_region(*arg, **kw):
102 return RhodeCodeCacheRegion(*arg, **kw)
102 return RhodeCodeCacheRegion(*arg, **kw)
103
103
104
104
105 def get_default_cache_settings(settings, prefixes=None):
105 def get_default_cache_settings(settings, prefixes=None):
106 prefixes = prefixes or []
106 prefixes = prefixes or []
107 cache_settings = {}
107 cache_settings = {}
108 for key in settings.keys():
108 for key in settings.keys():
109 for prefix in prefixes:
109 for prefix in prefixes:
110 if key.startswith(prefix):
110 if key.startswith(prefix):
111 name = key.split(prefix)[1].strip()
111 name = key.split(prefix)[1].strip()
112 val = settings[key]
112 val = settings[key]
113 if isinstance(val, basestring):
113 if isinstance(val, basestring):
114 val = val.strip()
114 val = val.strip()
115 cache_settings[name] = val
115 cache_settings[name] = val
116 return cache_settings
116 return cache_settings
117
117
118
118
119 def compute_key_from_params(*args):
119 def compute_key_from_params(*args):
120 """
120 """
121 Helper to compute key from given params to be used in cache manager
121 Helper to compute key from given params to be used in cache manager
122 """
122 """
123 return sha1("_".join(map(safe_str, args)))
123 return sha1("_".join(map(safe_str, args)))
124
124
125
125
126 def key_generator(namespace, fn):
126 def key_generator(namespace, fn):
127 fname = fn.__name__
127 fname = fn.__name__
128
128
129 def generate_key(*args):
129 def generate_key(*args):
130 namespace_pref = namespace or 'default'
130 namespace_pref = namespace or 'default'
131 arg_key = compute_key_from_params(*args)
131 arg_key = compute_key_from_params(*args)
132 final_key = "{}:{}_{}".format(namespace_pref, fname, arg_key)
132 final_key = "{}:{}_{}".format(namespace_pref, fname, arg_key)
133
133
134 return final_key
134 return final_key
135
135
136 return generate_key
136 return generate_key
@@ -1,386 +1,386 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """Handles the Git smart protocol."""
18 """Handles the Git smart protocol."""
19
19
20 import os
20 import os
21 import socket
21 import socket
22 import logging
22 import logging
23
23
24 import simplejson as json
24 import simplejson as json
25 import dulwich.protocol
25 import dulwich.protocol
26 from webob import Request, Response, exc
26 from webob import Request, Response, exc
27
27
28 from vcsserver import hooks, subprocessio
28 from vcsserver import hooks, subprocessio
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class FileWrapper(object):
34 class FileWrapper(object):
35 """File wrapper that ensures how much data is read from it."""
35 """File wrapper that ensures how much data is read from it."""
36
36
37 def __init__(self, fd, content_length):
37 def __init__(self, fd, content_length):
38 self.fd = fd
38 self.fd = fd
39 self.content_length = content_length
39 self.content_length = content_length
40 self.remain = content_length
40 self.remain = content_length
41
41
42 def read(self, size):
42 def read(self, size):
43 if size <= self.remain:
43 if size <= self.remain:
44 try:
44 try:
45 data = self.fd.read(size)
45 data = self.fd.read(size)
46 except socket.error:
46 except socket.error:
47 raise IOError(self)
47 raise IOError(self)
48 self.remain -= size
48 self.remain -= size
49 elif self.remain:
49 elif self.remain:
50 data = self.fd.read(self.remain)
50 data = self.fd.read(self.remain)
51 self.remain = 0
51 self.remain = 0
52 else:
52 else:
53 data = None
53 data = None
54 return data
54 return data
55
55
56 def __repr__(self):
56 def __repr__(self):
57 return '<FileWrapper %s len: %s, read: %s>' % (
57 return '<FileWrapper %s len: %s, read: %s>' % (
58 self.fd, self.content_length, self.content_length - self.remain
58 self.fd, self.content_length, self.content_length - self.remain
59 )
59 )
60
60
61
61
62 class GitRepository(object):
62 class GitRepository(object):
63 """WSGI app for handling Git smart protocol endpoints."""
63 """WSGI app for handling Git smart protocol endpoints."""
64
64
65 git_folder_signature = frozenset(
65 git_folder_signature = frozenset(
66 ('config', 'head', 'info', 'objects', 'refs'))
66 ('config', 'head', 'info', 'objects', 'refs'))
67 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
67 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 valid_accepts = frozenset(('application/x-%s-result' %
68 valid_accepts = frozenset(('application/x-%s-result' %
69 c for c in commands))
69 c for c in commands))
70
70
71 # The last bytes are the SHA1 of the first 12 bytes.
71 # The last bytes are the SHA1 of the first 12 bytes.
72 EMPTY_PACK = (
72 EMPTY_PACK = (
73 'PACK\x00\x00\x00\x02\x00\x00\x00\x00' +
73 'PACK\x00\x00\x00\x02\x00\x00\x00\x00' +
74 '\x02\x9d\x08\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
74 '\x02\x9d\x08\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
75 )
75 )
76 SIDE_BAND_CAPS = frozenset(('side-band', 'side-band-64k'))
76 SIDE_BAND_CAPS = frozenset(('side-band', 'side-band-64k'))
77
77
78 def __init__(self, repo_name, content_path, git_path, update_server_info,
78 def __init__(self, repo_name, content_path, git_path, update_server_info,
79 extras):
79 extras):
80 files = frozenset(f.lower() for f in os.listdir(content_path))
80 files = frozenset(f.lower() for f in os.listdir(content_path))
81 valid_dir_signature = self.git_folder_signature.issubset(files)
81 valid_dir_signature = self.git_folder_signature.issubset(files)
82
82
83 if not valid_dir_signature:
83 if not valid_dir_signature:
84 raise OSError('%s missing git signature' % content_path)
84 raise OSError('%s missing git signature' % content_path)
85
85
86 self.content_path = content_path
86 self.content_path = content_path
87 self.repo_name = repo_name
87 self.repo_name = repo_name
88 self.extras = extras
88 self.extras = extras
89 self.git_path = git_path
89 self.git_path = git_path
90 self.update_server_info = update_server_info
90 self.update_server_info = update_server_info
91
91
92 def _get_fixedpath(self, path):
92 def _get_fixedpath(self, path):
93 """
93 """
94 Small fix for repo_path
94 Small fix for repo_path
95
95
96 :param path:
96 :param path:
97 """
97 """
98 path = path.split(self.repo_name, 1)[-1]
98 path = path.split(self.repo_name, 1)[-1]
99 if path.startswith('.git'):
99 if path.startswith('.git'):
100 # for bare repos we still get the .git prefix inside, we skip it
100 # for bare repos we still get the .git prefix inside, we skip it
101 # here, and remove from the service command
101 # here, and remove from the service command
102 path = path[4:]
102 path = path[4:]
103
103
104 return path.strip('/')
104 return path.strip('/')
105
105
106 def inforefs(self, request, unused_environ):
106 def inforefs(self, request, unused_environ):
107 """
107 """
108 WSGI Response producer for HTTP GET Git Smart
108 WSGI Response producer for HTTP GET Git Smart
109 HTTP /info/refs request.
109 HTTP /info/refs request.
110 """
110 """
111
111
112 git_command = request.GET.get('service')
112 git_command = request.GET.get('service')
113 if git_command not in self.commands:
113 if git_command not in self.commands:
114 log.debug('command %s not allowed', git_command)
114 log.debug('command %s not allowed', git_command)
115 return exc.HTTPForbidden()
115 return exc.HTTPForbidden()
116
116
117 # please, resist the urge to add '\n' to git capture and increment
117 # please, resist the urge to add '\n' to git capture and increment
118 # line count by 1.
118 # line count by 1.
119 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
119 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
120 # a part of protocol.
120 # a part of protocol.
121 # The code in Git client not only does NOT need '\n', but actually
121 # The code in Git client not only does NOT need '\n', but actually
122 # blows up if you sprinkle "flush" (0000) as "0001\n".
122 # blows up if you sprinkle "flush" (0000) as "0001\n".
123 # It reads binary, per number of bytes specified.
123 # It reads binary, per number of bytes specified.
124 # if you do add '\n' as part of data, count it.
124 # if you do add '\n' as part of data, count it.
125 server_advert = '# service=%s\n' % git_command
125 server_advert = '# service=%s\n' % git_command
126 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
126 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
127 try:
127 try:
128 gitenv = dict(os.environ)
128 gitenv = dict(os.environ)
129 # forget all configs
129 # forget all configs
130 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
130 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
131 command = [self.git_path, git_command[4:], '--stateless-rpc',
131 command = [self.git_path, git_command[4:], '--stateless-rpc',
132 '--advertise-refs', self.content_path]
132 '--advertise-refs', self.content_path]
133 out = subprocessio.SubprocessIOChunker(
133 out = subprocessio.SubprocessIOChunker(
134 command,
134 command,
135 env=gitenv,
135 env=gitenv,
136 starting_values=[packet_len + server_advert + '0000'],
136 starting_values=[packet_len + server_advert + '0000'],
137 shell=False
137 shell=False
138 )
138 )
139 except EnvironmentError:
139 except EnvironmentError:
140 log.exception('Error processing command')
140 log.exception('Error processing command')
141 raise exc.HTTPExpectationFailed()
141 raise exc.HTTPExpectationFailed()
142
142
143 resp = Response()
143 resp = Response()
144 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
144 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
145 resp.charset = None
145 resp.charset = None
146 resp.app_iter = out
146 resp.app_iter = out
147
147
148 return resp
148 return resp
149
149
150 def _get_want_capabilities(self, request):
150 def _get_want_capabilities(self, request):
151 """Read the capabilities found in the first want line of the request."""
151 """Read the capabilities found in the first want line of the request."""
152 pos = request.body_file_seekable.tell()
152 pos = request.body_file_seekable.tell()
153 first_line = request.body_file_seekable.readline()
153 first_line = request.body_file_seekable.readline()
154 request.body_file_seekable.seek(pos)
154 request.body_file_seekable.seek(pos)
155
155
156 return frozenset(
156 return frozenset(
157 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
157 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
158
158
159 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
159 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
160 """
160 """
161 Construct a response with an empty PACK file.
161 Construct a response with an empty PACK file.
162
162
163 We use an empty PACK file, as that would trigger the failure of the pull
163 We use an empty PACK file, as that would trigger the failure of the pull
164 or clone command.
164 or clone command.
165
165
166 We also print in the error output a message explaining why the command
166 We also print in the error output a message explaining why the command
167 was aborted.
167 was aborted.
168
168
169 If aditionally, the user is accepting messages we send them the output
169 If aditionally, the user is accepting messages we send them the output
170 of the pre-pull hook.
170 of the pre-pull hook.
171
171
172 Note that for clients not supporting side-band we just send them the
172 Note that for clients not supporting side-band we just send them the
173 emtpy PACK file.
173 emtpy PACK file.
174 """
174 """
175 if self.SIDE_BAND_CAPS.intersection(capabilities):
175 if self.SIDE_BAND_CAPS.intersection(capabilities):
176 response = []
176 response = []
177 proto = dulwich.protocol.Protocol(None, response.append)
177 proto = dulwich.protocol.Protocol(None, response.append)
178 proto.write_pkt_line('NAK\n')
178 proto.write_pkt_line('NAK\n')
179 self._write_sideband_to_proto(pre_pull_messages, proto,
179 self._write_sideband_to_proto(pre_pull_messages, proto,
180 capabilities)
180 capabilities)
181 # N.B.(skreft): Do not change the sideband channel to 3, as that
181 # N.B.(skreft): Do not change the sideband channel to 3, as that
182 # produces a fatal error in the client:
182 # produces a fatal error in the client:
183 # fatal: error in sideband demultiplexer
183 # fatal: error in sideband demultiplexer
184 proto.write_sideband(2, 'Pre pull hook failed: aborting\n')
184 proto.write_sideband(2, 'Pre pull hook failed: aborting\n')
185 proto.write_sideband(1, self.EMPTY_PACK)
185 proto.write_sideband(1, self.EMPTY_PACK)
186
186
187 # writes 0000
187 # writes 0000
188 proto.write_pkt_line(None)
188 proto.write_pkt_line(None)
189
189
190 return response
190 return response
191 else:
191 else:
192 return [self.EMPTY_PACK]
192 return [self.EMPTY_PACK]
193
193
194 def _write_sideband_to_proto(self, data, proto, capabilities):
194 def _write_sideband_to_proto(self, data, proto, capabilities):
195 """
195 """
196 Write the data to the proto's sideband number 2.
196 Write the data to the proto's sideband number 2.
197
197
198 We do not use dulwich's write_sideband directly as it only supports
198 We do not use dulwich's write_sideband directly as it only supports
199 side-band-64k.
199 side-band-64k.
200 """
200 """
201 if not data:
201 if not data:
202 return
202 return
203
203
204 # N.B.(skreft): The values below are explained in the pack protocol
204 # N.B.(skreft): The values below are explained in the pack protocol
205 # documentation, section Packfile Data.
205 # documentation, section Packfile Data.
206 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
206 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
207 if 'side-band-64k' in capabilities:
207 if 'side-band-64k' in capabilities:
208 chunk_size = 65515
208 chunk_size = 65515
209 elif 'side-band' in capabilities:
209 elif 'side-band' in capabilities:
210 chunk_size = 995
210 chunk_size = 995
211 else:
211 else:
212 return
212 return
213
213
214 chunker = (
214 chunker = (
215 data[i:i + chunk_size] for i in xrange(0, len(data), chunk_size))
215 data[i:i + chunk_size] for i in xrange(0, len(data), chunk_size))
216
216
217 for chunk in chunker:
217 for chunk in chunker:
218 proto.write_sideband(2, chunk)
218 proto.write_sideband(2, chunk)
219
219
220 def _get_messages(self, data, capabilities):
220 def _get_messages(self, data, capabilities):
221 """Return a list with packets for sending data in sideband number 2."""
221 """Return a list with packets for sending data in sideband number 2."""
222 response = []
222 response = []
223 proto = dulwich.protocol.Protocol(None, response.append)
223 proto = dulwich.protocol.Protocol(None, response.append)
224
224
225 self._write_sideband_to_proto(data, proto, capabilities)
225 self._write_sideband_to_proto(data, proto, capabilities)
226
226
227 return response
227 return response
228
228
229 def _inject_messages_to_response(self, response, capabilities,
229 def _inject_messages_to_response(self, response, capabilities,
230 start_messages, end_messages):
230 start_messages, end_messages):
231 """
231 """
232 Given a list response we inject the pre/post-pull messages.
232 Given a list response we inject the pre/post-pull messages.
233
233
234 We only inject the messages if the client supports sideband, and the
234 We only inject the messages if the client supports sideband, and the
235 response has the format:
235 response has the format:
236 0008NAK\n...0000
236 0008NAK\n...0000
237
237
238 Note that we do not check the no-progress capability as by default, git
238 Note that we do not check the no-progress capability as by default, git
239 sends it, which effectively would block all messages.
239 sends it, which effectively would block all messages.
240 """
240 """
241 if not self.SIDE_BAND_CAPS.intersection(capabilities):
241 if not self.SIDE_BAND_CAPS.intersection(capabilities):
242 return response
242 return response
243
243
244 if not start_messages and not end_messages:
244 if not start_messages and not end_messages:
245 return response
245 return response
246
246
247 # make a list out of response if it's an iterator
247 # make a list out of response if it's an iterator
248 # so we can investigate it for message injection.
248 # so we can investigate it for message injection.
249 if hasattr(response, '__iter__'):
249 if hasattr(response, '__iter__'):
250 response = list(response)
250 response = list(response)
251
251
252 if (not response[0].startswith('0008NAK\n') or
252 if (not response[0].startswith('0008NAK\n') or
253 not response[-1].endswith('0000')):
253 not response[-1].endswith('0000')):
254 return response
254 return response
255
255
256 new_response = ['0008NAK\n']
256 new_response = ['0008NAK\n']
257 new_response.extend(self._get_messages(start_messages, capabilities))
257 new_response.extend(self._get_messages(start_messages, capabilities))
258 if len(response) == 1:
258 if len(response) == 1:
259 new_response.append(response[0][8:-4])
259 new_response.append(response[0][8:-4])
260 else:
260 else:
261 new_response.append(response[0][8:])
261 new_response.append(response[0][8:])
262 new_response.extend(response[1:-1])
262 new_response.extend(response[1:-1])
263 new_response.append(response[-1][:-4])
263 new_response.append(response[-1][:-4])
264 new_response.extend(self._get_messages(end_messages, capabilities))
264 new_response.extend(self._get_messages(end_messages, capabilities))
265 new_response.append('0000')
265 new_response.append('0000')
266
266
267 return new_response
267 return new_response
268
268
269 def backend(self, request, environ):
269 def backend(self, request, environ):
270 """
270 """
271 WSGI Response producer for HTTP POST Git Smart HTTP requests.
271 WSGI Response producer for HTTP POST Git Smart HTTP requests.
272 Reads commands and data from HTTP POST's body.
272 Reads commands and data from HTTP POST's body.
273 returns an iterator obj with contents of git command's
273 returns an iterator obj with contents of git command's
274 response to stdout
274 response to stdout
275 """
275 """
276 # TODO(skreft): think how we could detect an HTTPLockedException, as
276 # TODO(skreft): think how we could detect an HTTPLockedException, as
277 # we probably want to have the same mechanism used by mercurial and
277 # we probably want to have the same mechanism used by mercurial and
278 # simplevcs.
278 # simplevcs.
279 # For that we would need to parse the output of the command looking for
279 # For that we would need to parse the output of the command looking for
280 # some signs of the HTTPLockedError, parse the data and reraise it in
280 # some signs of the HTTPLockedError, parse the data and reraise it in
281 # pygrack. However, that would interfere with the streaming.
281 # pygrack. However, that would interfere with the streaming.
282 #
282 #
283 # Now the output of a blocked push is:
283 # Now the output of a blocked push is:
284 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
284 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
285 # POST git-receive-pack (1047 bytes)
285 # POST git-receive-pack (1047 bytes)
286 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
286 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
287 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
287 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
288 # ! [remote rejected] master -> master (pre-receive hook declined)
288 # ! [remote rejected] master -> master (pre-receive hook declined)
289 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
289 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
290
290
291 git_command = self._get_fixedpath(request.path_info)
291 git_command = self._get_fixedpath(request.path_info)
292 if git_command not in self.commands:
292 if git_command not in self.commands:
293 log.debug('command %s not allowed', git_command)
293 log.debug('command %s not allowed', git_command)
294 return exc.HTTPForbidden()
294 return exc.HTTPForbidden()
295
295
296 capabilities = None
296 capabilities = None
297 if git_command == 'git-upload-pack':
297 if git_command == 'git-upload-pack':
298 capabilities = self._get_want_capabilities(request)
298 capabilities = self._get_want_capabilities(request)
299
299
300 if 'CONTENT_LENGTH' in environ:
300 if 'CONTENT_LENGTH' in environ:
301 inputstream = FileWrapper(request.body_file_seekable,
301 inputstream = FileWrapper(request.body_file_seekable,
302 request.content_length)
302 request.content_length)
303 else:
303 else:
304 inputstream = request.body_file_seekable
304 inputstream = request.body_file_seekable
305
305
306 resp = Response()
306 resp = Response()
307 resp.content_type = ('application/x-%s-result' %
307 resp.content_type = ('application/x-%s-result' %
308 git_command.encode('utf8'))
308 git_command.encode('utf8'))
309 resp.charset = None
309 resp.charset = None
310
310
311 pre_pull_messages = ''
311 pre_pull_messages = ''
312 if git_command == 'git-upload-pack':
312 if git_command == 'git-upload-pack':
313 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
313 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
314 if status != 0:
314 if status != 0:
315 resp.app_iter = self._build_failed_pre_pull_response(
315 resp.app_iter = self._build_failed_pre_pull_response(
316 capabilities, pre_pull_messages)
316 capabilities, pre_pull_messages)
317 return resp
317 return resp
318
318
319 gitenv = dict(os.environ)
319 gitenv = dict(os.environ)
320 # forget all configs
320 # forget all configs
321 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
321 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
322 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
322 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
323 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
323 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
324 self.content_path]
324 self.content_path]
325 log.debug('handling cmd %s', cmd)
325 log.debug('handling cmd %s', cmd)
326
326
327 out = subprocessio.SubprocessIOChunker(
327 out = subprocessio.SubprocessIOChunker(
328 cmd,
328 cmd,
329 inputstream=inputstream,
329 inputstream=inputstream,
330 env=gitenv,
330 env=gitenv,
331 cwd=self.content_path,
331 cwd=self.content_path,
332 shell=False,
332 shell=False,
333 fail_on_stderr=False,
333 fail_on_stderr=False,
334 fail_on_return_code=False
334 fail_on_return_code=False
335 )
335 )
336
336
337 if self.update_server_info and git_command == 'git-receive-pack':
337 if self.update_server_info and git_command == 'git-receive-pack':
338 # We need to fully consume the iterator here, as the
338 # We need to fully consume the iterator here, as the
339 # update-server-info command needs to be run after the push.
339 # update-server-info command needs to be run after the push.
340 out = list(out)
340 out = list(out)
341
341
342 # Updating refs manually after each push.
342 # Updating refs manually after each push.
343 # This is required as some clients are exposing Git repos internally
343 # This is required as some clients are exposing Git repos internally
344 # with the dumb protocol.
344 # with the dumb protocol.
345 cmd = [self.git_path, 'update-server-info']
345 cmd = [self.git_path, 'update-server-info']
346 log.debug('handling cmd %s', cmd)
346 log.debug('handling cmd %s', cmd)
347 output = subprocessio.SubprocessIOChunker(
347 output = subprocessio.SubprocessIOChunker(
348 cmd,
348 cmd,
349 inputstream=inputstream,
349 inputstream=inputstream,
350 env=gitenv,
350 env=gitenv,
351 cwd=self.content_path,
351 cwd=self.content_path,
352 shell=False,
352 shell=False,
353 fail_on_stderr=False,
353 fail_on_stderr=False,
354 fail_on_return_code=False
354 fail_on_return_code=False
355 )
355 )
356 # Consume all the output so the subprocess finishes
356 # Consume all the output so the subprocess finishes
357 for _ in output:
357 for _ in output:
358 pass
358 pass
359
359
360 if git_command == 'git-upload-pack':
360 if git_command == 'git-upload-pack':
361 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
361 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
362 resp.app_iter = self._inject_messages_to_response(
362 resp.app_iter = self._inject_messages_to_response(
363 out, capabilities, pre_pull_messages, post_pull_messages)
363 out, capabilities, pre_pull_messages, post_pull_messages)
364 else:
364 else:
365 resp.app_iter = out
365 resp.app_iter = out
366
366
367 return resp
367 return resp
368
368
369 def __call__(self, environ, start_response):
369 def __call__(self, environ, start_response):
370 request = Request(environ)
370 request = Request(environ)
371 _path = self._get_fixedpath(request.path_info)
371 _path = self._get_fixedpath(request.path_info)
372 if _path.startswith('info/refs'):
372 if _path.startswith('info/refs'):
373 app = self.inforefs
373 app = self.inforefs
374 else:
374 else:
375 app = self.backend
375 app = self.backend
376
376
377 try:
377 try:
378 resp = app(request, environ)
378 resp = app(request, environ)
379 except exc.HTTPException as error:
379 except exc.HTTPException as error:
380 log.exception('HTTP Error')
380 log.exception('HTTP Error')
381 resp = error
381 resp = error
382 except Exception:
382 except Exception:
383 log.exception('Unknown error')
383 log.exception('Unknown error')
384 resp = exc.HTTPInternalServerError()
384 resp = exc.HTTPInternalServerError()
385
385
386 return resp(environ, start_response)
386 return resp(environ, start_response)
@@ -1,34 +1,34 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 from vcsserver import scm_app, wsgi_app_caller
18 from vcsserver import scm_app, wsgi_app_caller
19
19
20
20
21 class GitRemoteWsgi(object):
21 class GitRemoteWsgi(object):
22 def handle(self, environ, input_data, *args, **kwargs):
22 def handle(self, environ, input_data, *args, **kwargs):
23 app = wsgi_app_caller.WSGIAppCaller(
23 app = wsgi_app_caller.WSGIAppCaller(
24 scm_app.create_git_wsgi_app(*args, **kwargs))
24 scm_app.create_git_wsgi_app(*args, **kwargs))
25
25
26 return app.handle(environ, input_data)
26 return app.handle(environ, input_data)
27
27
28
28
29 class HgRemoteWsgi(object):
29 class HgRemoteWsgi(object):
30 def handle(self, environ, input_data, *args, **kwargs):
30 def handle(self, environ, input_data, *args, **kwargs):
31 app = wsgi_app_caller.WSGIAppCaller(
31 app = wsgi_app_caller.WSGIAppCaller(
32 scm_app.create_hg_wsgi_app(*args, **kwargs))
32 scm_app.create_hg_wsgi_app(*args, **kwargs))
33
33
34 return app.handle(environ, input_data)
34 return app.handle(environ, input_data)
@@ -1,234 +1,234 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import logging
19 import logging
20 import itertools
20 import itertools
21
21
22 import mercurial
22 import mercurial
23 import mercurial.error
23 import mercurial.error
24 import mercurial.wireprotoserver
24 import mercurial.wireprotoserver
25 import mercurial.hgweb.common
25 import mercurial.hgweb.common
26 import mercurial.hgweb.hgweb_mod
26 import mercurial.hgweb.hgweb_mod
27 import webob.exc
27 import webob.exc
28
28
29 from vcsserver import pygrack, exceptions, settings, git_lfs
29 from vcsserver import pygrack, exceptions, settings, git_lfs
30
30
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 # propagated from mercurial documentation
35 # propagated from mercurial documentation
36 HG_UI_SECTIONS = [
36 HG_UI_SECTIONS = [
37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
40 ]
40 ]
41
41
42
42
43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
44 """Extension of hgweb that simplifies some functions."""
44 """Extension of hgweb that simplifies some functions."""
45
45
46 def _get_view(self, repo):
46 def _get_view(self, repo):
47 """Views are not supported."""
47 """Views are not supported."""
48 return repo
48 return repo
49
49
50 def loadsubweb(self):
50 def loadsubweb(self):
51 """The result is only used in the templater method which is not used."""
51 """The result is only used in the templater method which is not used."""
52 return None
52 return None
53
53
54 def run(self):
54 def run(self):
55 """Unused function so raise an exception if accidentally called."""
55 """Unused function so raise an exception if accidentally called."""
56 raise NotImplementedError
56 raise NotImplementedError
57
57
58 def templater(self, req):
58 def templater(self, req):
59 """Function used in an unreachable code path.
59 """Function used in an unreachable code path.
60
60
61 This code is unreachable because we guarantee that the HTTP request,
61 This code is unreachable because we guarantee that the HTTP request,
62 corresponds to a Mercurial command. See the is_hg method. So, we are
62 corresponds to a Mercurial command. See the is_hg method. So, we are
63 never going to get a user-visible url.
63 never going to get a user-visible url.
64 """
64 """
65 raise NotImplementedError
65 raise NotImplementedError
66
66
67 def archivelist(self, nodeid):
67 def archivelist(self, nodeid):
68 """Unused function so raise an exception if accidentally called."""
68 """Unused function so raise an exception if accidentally called."""
69 raise NotImplementedError
69 raise NotImplementedError
70
70
71 def __call__(self, environ, start_response):
71 def __call__(self, environ, start_response):
72 """Run the WSGI application.
72 """Run the WSGI application.
73
73
74 This may be called by multiple threads.
74 This may be called by multiple threads.
75 """
75 """
76 from mercurial.hgweb import request as requestmod
76 from mercurial.hgweb import request as requestmod
77 req = requestmod.parserequestfromenv(environ)
77 req = requestmod.parserequestfromenv(environ)
78 res = requestmod.wsgiresponse(req, start_response)
78 res = requestmod.wsgiresponse(req, start_response)
79 gen = self.run_wsgi(req, res)
79 gen = self.run_wsgi(req, res)
80
80
81 first_chunk = None
81 first_chunk = None
82
82
83 try:
83 try:
84 data = gen.next()
84 data = gen.next()
85
85
86 def first_chunk():
86 def first_chunk():
87 yield data
87 yield data
88 except StopIteration:
88 except StopIteration:
89 pass
89 pass
90
90
91 if first_chunk:
91 if first_chunk:
92 return itertools.chain(first_chunk(), gen)
92 return itertools.chain(first_chunk(), gen)
93 return gen
93 return gen
94
94
95 def _runwsgi(self, req, res, repo):
95 def _runwsgi(self, req, res, repo):
96
96
97 cmd = req.qsparams.get('cmd', '')
97 cmd = req.qsparams.get('cmd', '')
98 if not mercurial.wireprotoserver.iscmd(cmd):
98 if not mercurial.wireprotoserver.iscmd(cmd):
99 # NOTE(marcink): for unsupported commands, we return bad request
99 # NOTE(marcink): for unsupported commands, we return bad request
100 # internally from HG
100 # internally from HG
101 from mercurial.hgweb.common import statusmessage
101 from mercurial.hgweb.common import statusmessage
102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
103 res.setbodybytes('')
103 res.setbodybytes('')
104 return res.sendresponse()
104 return res.sendresponse()
105
105
106 return super(HgWeb, self)._runwsgi(req, res, repo)
106 return super(HgWeb, self)._runwsgi(req, res, repo)
107
107
108
108
109 def make_hg_ui_from_config(repo_config):
109 def make_hg_ui_from_config(repo_config):
110 baseui = mercurial.ui.ui()
110 baseui = mercurial.ui.ui()
111
111
112 # clean the baseui object
112 # clean the baseui object
113 baseui._ocfg = mercurial.config.config()
113 baseui._ocfg = mercurial.config.config()
114 baseui._ucfg = mercurial.config.config()
114 baseui._ucfg = mercurial.config.config()
115 baseui._tcfg = mercurial.config.config()
115 baseui._tcfg = mercurial.config.config()
116
116
117 for section, option, value in repo_config:
117 for section, option, value in repo_config:
118 baseui.setconfig(section, option, value)
118 baseui.setconfig(section, option, value)
119
119
120 # make our hgweb quiet so it doesn't print output
120 # make our hgweb quiet so it doesn't print output
121 baseui.setconfig('ui', 'quiet', 'true')
121 baseui.setconfig('ui', 'quiet', 'true')
122
122
123 return baseui
123 return baseui
124
124
125
125
126 def update_hg_ui_from_hgrc(baseui, repo_path):
126 def update_hg_ui_from_hgrc(baseui, repo_path):
127 path = os.path.join(repo_path, '.hg', 'hgrc')
127 path = os.path.join(repo_path, '.hg', 'hgrc')
128
128
129 if not os.path.isfile(path):
129 if not os.path.isfile(path):
130 log.debug('hgrc file is not present at %s, skipping...', path)
130 log.debug('hgrc file is not present at %s, skipping...', path)
131 return
131 return
132 log.debug('reading hgrc from %s', path)
132 log.debug('reading hgrc from %s', path)
133 cfg = mercurial.config.config()
133 cfg = mercurial.config.config()
134 cfg.read(path)
134 cfg.read(path)
135 for section in HG_UI_SECTIONS:
135 for section in HG_UI_SECTIONS:
136 for k, v in cfg.items(section):
136 for k, v in cfg.items(section):
137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
138 baseui.setconfig(section, k, v)
138 baseui.setconfig(section, k, v)
139
139
140
140
141 def create_hg_wsgi_app(repo_path, repo_name, config):
141 def create_hg_wsgi_app(repo_path, repo_name, config):
142 """
142 """
143 Prepares a WSGI application to handle Mercurial requests.
143 Prepares a WSGI application to handle Mercurial requests.
144
144
145 :param config: is a list of 3-item tuples representing a ConfigObject
145 :param config: is a list of 3-item tuples representing a ConfigObject
146 (it is the serialized version of the config object).
146 (it is the serialized version of the config object).
147 """
147 """
148 log.debug("Creating Mercurial WSGI application")
148 log.debug("Creating Mercurial WSGI application")
149
149
150 baseui = make_hg_ui_from_config(config)
150 baseui = make_hg_ui_from_config(config)
151 update_hg_ui_from_hgrc(baseui, repo_path)
151 update_hg_ui_from_hgrc(baseui, repo_path)
152
152
153 try:
153 try:
154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
155 except mercurial.error.RequirementError as e:
155 except mercurial.error.RequirementError as e:
156 raise exceptions.RequirementException(e)(e)
156 raise exceptions.RequirementException(e)(e)
157
157
158
158
159 class GitHandler(object):
159 class GitHandler(object):
160 """
160 """
161 Handler for Git operations like push/pull etc
161 Handler for Git operations like push/pull etc
162 """
162 """
163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
164 extras):
164 extras):
165 if not os.path.isdir(repo_location):
165 if not os.path.isdir(repo_location):
166 raise OSError(repo_location)
166 raise OSError(repo_location)
167 self.content_path = repo_location
167 self.content_path = repo_location
168 self.repo_name = repo_name
168 self.repo_name = repo_name
169 self.repo_location = repo_location
169 self.repo_location = repo_location
170 self.extras = extras
170 self.extras = extras
171 self.git_path = git_path
171 self.git_path = git_path
172 self.update_server_info = update_server_info
172 self.update_server_info = update_server_info
173
173
174 def __call__(self, environ, start_response):
174 def __call__(self, environ, start_response):
175 app = webob.exc.HTTPNotFound()
175 app = webob.exc.HTTPNotFound()
176 candidate_paths = (
176 candidate_paths = (
177 self.content_path, os.path.join(self.content_path, '.git'))
177 self.content_path, os.path.join(self.content_path, '.git'))
178
178
179 for content_path in candidate_paths:
179 for content_path in candidate_paths:
180 try:
180 try:
181 app = pygrack.GitRepository(
181 app = pygrack.GitRepository(
182 self.repo_name, content_path, self.git_path,
182 self.repo_name, content_path, self.git_path,
183 self.update_server_info, self.extras)
183 self.update_server_info, self.extras)
184 break
184 break
185 except OSError:
185 except OSError:
186 continue
186 continue
187
187
188 return app(environ, start_response)
188 return app(environ, start_response)
189
189
190
190
191 def create_git_wsgi_app(repo_path, repo_name, config):
191 def create_git_wsgi_app(repo_path, repo_name, config):
192 """
192 """
193 Creates a WSGI application to handle Git requests.
193 Creates a WSGI application to handle Git requests.
194
194
195 :param config: is a dictionary holding the extras.
195 :param config: is a dictionary holding the extras.
196 """
196 """
197 git_path = settings.GIT_EXECUTABLE
197 git_path = settings.GIT_EXECUTABLE
198 update_server_info = config.pop('git_update_server_info')
198 update_server_info = config.pop('git_update_server_info')
199 app = GitHandler(
199 app = GitHandler(
200 repo_path, repo_name, git_path, update_server_info, config)
200 repo_path, repo_name, git_path, update_server_info, config)
201
201
202 return app
202 return app
203
203
204
204
205 class GitLFSHandler(object):
205 class GitLFSHandler(object):
206 """
206 """
207 Handler for Git LFS operations
207 Handler for Git LFS operations
208 """
208 """
209
209
210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
211 extras):
211 extras):
212 if not os.path.isdir(repo_location):
212 if not os.path.isdir(repo_location):
213 raise OSError(repo_location)
213 raise OSError(repo_location)
214 self.content_path = repo_location
214 self.content_path = repo_location
215 self.repo_name = repo_name
215 self.repo_name = repo_name
216 self.repo_location = repo_location
216 self.repo_location = repo_location
217 self.extras = extras
217 self.extras = extras
218 self.git_path = git_path
218 self.git_path = git_path
219 self.update_server_info = update_server_info
219 self.update_server_info = update_server_info
220
220
221 def get_app(self, git_lfs_enabled, git_lfs_store_path):
221 def get_app(self, git_lfs_enabled, git_lfs_store_path):
222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path)
222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path)
223 return app
223 return app
224
224
225
225
226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
227 git_path = settings.GIT_EXECUTABLE
227 git_path = settings.GIT_EXECUTABLE
228 update_server_info = config.pop('git_update_server_info')
228 update_server_info = config.pop('git_update_server_info')
229 git_lfs_enabled = config.pop('git_lfs_enabled')
229 git_lfs_enabled = config.pop('git_lfs_enabled')
230 git_lfs_store_path = config.pop('git_lfs_store_path')
230 git_lfs_store_path = config.pop('git_lfs_store_path')
231 app = GitLFSHandler(
231 app = GitLFSHandler(
232 repo_path, repo_name, git_path, update_server_info, config)
232 repo_path, repo_name, git_path, update_server_info, config)
233
233
234 return app.get_app(git_lfs_enabled, git_lfs_store_path)
234 return app.get_app(git_lfs_enabled, git_lfs_store_path)
@@ -1,78 +1,78 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import gc
18 import gc
19 import logging
19 import logging
20 import os
20 import os
21 import time
21 import time
22
22
23
23
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 class VcsServer(object):
27 class VcsServer(object):
28 """
28 """
29 Exposed remote interface of the vcsserver itself.
29 Exposed remote interface of the vcsserver itself.
30
30
31 This object can be used to manage the server remotely. Right now the main
31 This object can be used to manage the server remotely. Right now the main
32 use case is to allow to shut down the server.
32 use case is to allow to shut down the server.
33 """
33 """
34
34
35 _shutdown = False
35 _shutdown = False
36
36
37 def shutdown(self):
37 def shutdown(self):
38 self._shutdown = True
38 self._shutdown = True
39
39
40 def ping(self):
40 def ping(self):
41 """
41 """
42 Utility to probe a server connection.
42 Utility to probe a server connection.
43 """
43 """
44 log.debug("Received server ping.")
44 log.debug("Received server ping.")
45
45
46 def echo(self, data):
46 def echo(self, data):
47 """
47 """
48 Utility for performance testing.
48 Utility for performance testing.
49
49
50 Allows to pass in arbitrary data and will return this data.
50 Allows to pass in arbitrary data and will return this data.
51 """
51 """
52 log.debug("Received server echo.")
52 log.debug("Received server echo.")
53 return data
53 return data
54
54
55 def sleep(self, seconds):
55 def sleep(self, seconds):
56 """
56 """
57 Utility to simulate long running server interaction.
57 Utility to simulate long running server interaction.
58 """
58 """
59 log.debug("Sleeping %s seconds", seconds)
59 log.debug("Sleeping %s seconds", seconds)
60 time.sleep(seconds)
60 time.sleep(seconds)
61
61
62 def get_pid(self):
62 def get_pid(self):
63 """
63 """
64 Allows to discover the PID based on a proxy object.
64 Allows to discover the PID based on a proxy object.
65 """
65 """
66 return os.getpid()
66 return os.getpid()
67
67
68 def run_gc(self):
68 def run_gc(self):
69 """
69 """
70 Allows to trigger the garbage collector.
70 Allows to trigger the garbage collector.
71
71
72 Main intention is to support statistics gathering during test runs.
72 Main intention is to support statistics gathering during test runs.
73 """
73 """
74 freed_objects = gc.collect()
74 freed_objects = gc.collect()
75 return {
75 return {
76 'freed_objects': freed_objects,
76 'freed_objects': freed_objects,
77 'garbage': len(gc.garbage),
77 'garbage': len(gc.garbage),
78 }
78 }
@@ -1,20 +1,20 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 WIRE_ENCODING = 'UTF-8'
18 WIRE_ENCODING = 'UTF-8'
19 GIT_EXECUTABLE = 'git'
19 GIT_EXECUTABLE = 'git'
20 BINARY_DIR = ''
20 BINARY_DIR = ''
@@ -1,516 +1,523 b''
1 """
1 """
2 Module provides a class allowing to wrap communication over subprocess.Popen
2 Module provides a class allowing to wrap communication over subprocess.Popen
3 input, output, error streams into a meaningfull, non-blocking, concurrent
3 input, output, error streams into a meaningfull, non-blocking, concurrent
4 stream processor exposing the output data as an iterator fitting to be a
4 stream processor exposing the output data as an iterator fitting to be a
5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
6
6
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
8
8
9 This file is part of git_http_backend.py Project.
9 This file is part of git_http_backend.py Project.
10
10
11 git_http_backend.py Project is free software: you can redistribute it and/or
11 git_http_backend.py Project is free software: you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public License as
12 modify it under the terms of the GNU Lesser General Public License as
13 published by the Free Software Foundation, either version 2.1 of the License,
13 published by the Free Software Foundation, either version 2.1 of the License,
14 or (at your option) any later version.
14 or (at your option) any later version.
15
15
16 git_http_backend.py Project is distributed in the hope that it will be useful,
16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU Lesser General Public License for more details.
19 GNU Lesser General Public License for more details.
20
20
21 You should have received a copy of the GNU Lesser General Public License
21 You should have received a copy of the GNU Lesser General Public License
22 along with git_http_backend.py Project.
22 along with git_http_backend.py Project.
23 If not, see <http://www.gnu.org/licenses/>.
23 If not, see <http://www.gnu.org/licenses/>.
24 """
24 """
25 import os
25 import os
26 import logging
26 import logging
27 import subprocess32 as subprocess
27 import subprocess32 as subprocess
28 from collections import deque
28 from collections import deque
29 from threading import Event, Thread
29 from threading import Event, Thread
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class StreamFeeder(Thread):
34 class StreamFeeder(Thread):
35 """
35 """
36 Normal writing into pipe-like is blocking once the buffer is filled.
36 Normal writing into pipe-like is blocking once the buffer is filled.
37 This thread allows a thread to seep data from a file-like into a pipe
37 This thread allows a thread to seep data from a file-like into a pipe
38 without blocking the main thread.
38 without blocking the main thread.
39 We close inpipe once the end of the source stream is reached.
39 We close inpipe once the end of the source stream is reached.
40 """
40 """
41
41
42 def __init__(self, source):
42 def __init__(self, source):
43 super(StreamFeeder, self).__init__()
43 super(StreamFeeder, self).__init__()
44 self.daemon = True
44 self.daemon = True
45 filelike = False
45 filelike = False
46 self.bytes = bytes()
46 self.bytes = bytes()
47 if type(source) in (type(''), bytes, bytearray): # string-like
47 if type(source) in (type(''), bytes, bytearray): # string-like
48 self.bytes = bytes(source)
48 self.bytes = bytes(source)
49 else: # can be either file pointer or file-like
49 else: # can be either file pointer or file-like
50 if type(source) in (int, long): # file pointer it is
50 if type(source) in (int, long): # file pointer it is
51 # converting file descriptor (int) stdin into file-like
51 # converting file descriptor (int) stdin into file-like
52 try:
52 try:
53 source = os.fdopen(source, 'rb', 16384)
53 source = os.fdopen(source, 'rb', 16384)
54 except Exception:
54 except Exception:
55 pass
55 pass
56 # let's see if source is file-like by now
56 # let's see if source is file-like by now
57 try:
57 try:
58 filelike = source.read
58 filelike = source.read
59 except Exception:
59 except Exception:
60 pass
60 pass
61 if not filelike and not self.bytes:
61 if not filelike and not self.bytes:
62 raise TypeError("StreamFeeder's source object must be a readable "
62 raise TypeError("StreamFeeder's source object must be a readable "
63 "file-like, a file descriptor, or a string-like.")
63 "file-like, a file descriptor, or a string-like.")
64 self.source = source
64 self.source = source
65 self.readiface, self.writeiface = os.pipe()
65 self.readiface, self.writeiface = os.pipe()
66
66
67 def run(self):
67 def run(self):
68 t = self.writeiface
68 t = self.writeiface
69 try:
69 if self.bytes:
70 if self.bytes:
70 os.write(t, self.bytes)
71 os.write(t, self.bytes)
71 else:
72 else:
72 s = self.source
73 s = self.source
73 b = s.read(4096)
74 b = s.read(4096)
74 while b:
75 while b:
75 os.write(t, b)
76 os.write(t, b)
76 b = s.read(4096)
77 b = s.read(4096)
78 finally:
77 os.close(t)
79 os.close(t)
78
80
79 @property
81 @property
80 def output(self):
82 def output(self):
81 return self.readiface
83 return self.readiface
82
84
83
85
84 class InputStreamChunker(Thread):
86 class InputStreamChunker(Thread):
85 def __init__(self, source, target, buffer_size, chunk_size):
87 def __init__(self, source, target, buffer_size, chunk_size):
86
88
87 super(InputStreamChunker, self).__init__()
89 super(InputStreamChunker, self).__init__()
88
90
89 self.daemon = True # die die die.
91 self.daemon = True # die die die.
90
92
91 self.source = source
93 self.source = source
92 self.target = target
94 self.target = target
93 self.chunk_count_max = int(buffer_size / chunk_size) + 1
95 self.chunk_count_max = int(buffer_size / chunk_size) + 1
94 self.chunk_size = chunk_size
96 self.chunk_size = chunk_size
95
97
96 self.data_added = Event()
98 self.data_added = Event()
97 self.data_added.clear()
99 self.data_added.clear()
98
100
99 self.keep_reading = Event()
101 self.keep_reading = Event()
100 self.keep_reading.set()
102 self.keep_reading.set()
101
103
102 self.EOF = Event()
104 self.EOF = Event()
103 self.EOF.clear()
105 self.EOF.clear()
104
106
105 self.go = Event()
107 self.go = Event()
106 self.go.set()
108 self.go.set()
107
109
108 def stop(self):
110 def stop(self):
109 self.go.clear()
111 self.go.clear()
110 self.EOF.set()
112 self.EOF.set()
111 try:
113 try:
112 # this is not proper, but is done to force the reader thread let
114 # this is not proper, but is done to force the reader thread let
113 # go of the input because, if successful, .close() will send EOF
115 # go of the input because, if successful, .close() will send EOF
114 # down the pipe.
116 # down the pipe.
115 self.source.close()
117 self.source.close()
116 except:
118 except:
117 pass
119 pass
118
120
119 def run(self):
121 def run(self):
120 s = self.source
122 s = self.source
121 t = self.target
123 t = self.target
122 cs = self.chunk_size
124 cs = self.chunk_size
123 chunk_count_max = self.chunk_count_max
125 chunk_count_max = self.chunk_count_max
124 keep_reading = self.keep_reading
126 keep_reading = self.keep_reading
125 da = self.data_added
127 da = self.data_added
126 go = self.go
128 go = self.go
127
129
128 try:
130 try:
129 b = s.read(cs)
131 b = s.read(cs)
130 except ValueError:
132 except ValueError:
131 b = ''
133 b = ''
132
134
133 timeout_input = 20
135 timeout_input = 20
134 while b and go.is_set():
136 while b and go.is_set():
135 if len(t) > chunk_count_max:
137 if len(t) > chunk_count_max:
136 keep_reading.clear()
138 keep_reading.clear()
137 keep_reading.wait(timeout_input)
139 keep_reading.wait(timeout_input)
138 if len(t) > chunk_count_max + timeout_input:
140 if len(t) > chunk_count_max + timeout_input:
139 log.error("Timed out while waiting for input from subprocess.")
141 log.error("Timed out while waiting for input from subprocess.")
140 os._exit(-1) # this will cause the worker to recycle itself
142 os._exit(-1) # this will cause the worker to recycle itself
141
143
142 t.append(b)
144 t.append(b)
143 da.set()
145 da.set()
144
146
145 try:
147 try:
146 b = s.read(cs)
148 b = s.read(cs)
147 except ValueError:
149 except ValueError:
148 b = ''
150 b = ''
149
151
150 self.EOF.set()
152 self.EOF.set()
151 da.set() # for cases when done but there was no input.
153 da.set() # for cases when done but there was no input.
152
154
153
155
154 class BufferedGenerator(object):
156 class BufferedGenerator(object):
155 """
157 """
156 Class behaves as a non-blocking, buffered pipe reader.
158 Class behaves as a non-blocking, buffered pipe reader.
157 Reads chunks of data (through a thread)
159 Reads chunks of data (through a thread)
158 from a blocking pipe, and attaches these to an array (Deque) of chunks.
160 from a blocking pipe, and attaches these to an array (Deque) of chunks.
159 Reading is halted in the thread when max chunks is internally buffered.
161 Reading is halted in the thread when max chunks is internally buffered.
160 The .next() may operate in blocking or non-blocking fashion by yielding
162 The .next() may operate in blocking or non-blocking fashion by yielding
161 '' if no data is ready
163 '' if no data is ready
162 to be sent or by not returning until there is some data to send
164 to be sent or by not returning until there is some data to send
163 When we get EOF from underlying source pipe we raise the marker to raise
165 When we get EOF from underlying source pipe we raise the marker to raise
164 StopIteration after the last chunk of data is yielded.
166 StopIteration after the last chunk of data is yielded.
165 """
167 """
166
168
167 def __init__(self, source, buffer_size=65536, chunk_size=4096,
169 def __init__(self, source, buffer_size=65536, chunk_size=4096,
168 starting_values=None, bottomless=False):
170 starting_values=None, bottomless=False):
169 starting_values = starting_values or []
171 starting_values = starting_values or []
170
172
171 if bottomless:
173 if bottomless:
172 maxlen = int(buffer_size / chunk_size)
174 maxlen = int(buffer_size / chunk_size)
173 else:
175 else:
174 maxlen = None
176 maxlen = None
175
177
176 self.data = deque(starting_values, maxlen)
178 self.data = deque(starting_values, maxlen)
177 self.worker = InputStreamChunker(source, self.data, buffer_size,
179 self.worker = InputStreamChunker(source, self.data, buffer_size,
178 chunk_size)
180 chunk_size)
179 if starting_values:
181 if starting_values:
180 self.worker.data_added.set()
182 self.worker.data_added.set()
181 self.worker.start()
183 self.worker.start()
182
184
183 ####################
185 ####################
184 # Generator's methods
186 # Generator's methods
185 ####################
187 ####################
186
188
187 def __iter__(self):
189 def __iter__(self):
188 return self
190 return self
189
191
190 def next(self):
192 def next(self):
191 while not len(self.data) and not self.worker.EOF.is_set():
193 while not len(self.data) and not self.worker.EOF.is_set():
192 self.worker.data_added.clear()
194 self.worker.data_added.clear()
193 self.worker.data_added.wait(0.2)
195 self.worker.data_added.wait(0.2)
194 if len(self.data):
196 if len(self.data):
195 self.worker.keep_reading.set()
197 self.worker.keep_reading.set()
196 return bytes(self.data.popleft())
198 return bytes(self.data.popleft())
197 elif self.worker.EOF.is_set():
199 elif self.worker.EOF.is_set():
198 raise StopIteration
200 raise StopIteration
199
201
200 def throw(self, exc_type, value=None, traceback=None):
202 def throw(self, exc_type, value=None, traceback=None):
201 if not self.worker.EOF.is_set():
203 if not self.worker.EOF.is_set():
202 raise exc_type(value)
204 raise exc_type(value)
203
205
204 def start(self):
206 def start(self):
205 self.worker.start()
207 self.worker.start()
206
208
207 def stop(self):
209 def stop(self):
208 self.worker.stop()
210 self.worker.stop()
209
211
210 def close(self):
212 def close(self):
211 try:
213 try:
212 self.worker.stop()
214 self.worker.stop()
213 self.throw(GeneratorExit)
215 self.throw(GeneratorExit)
214 except (GeneratorExit, StopIteration):
216 except (GeneratorExit, StopIteration):
215 pass
217 pass
216
218
217 def __del__(self):
219 def __del__(self):
218 self.close()
220 self.close()
219
221
220 ####################
222 ####################
221 # Threaded reader's infrastructure.
223 # Threaded reader's infrastructure.
222 ####################
224 ####################
223 @property
225 @property
224 def input(self):
226 def input(self):
225 return self.worker.w
227 return self.worker.w
226
228
227 @property
229 @property
228 def data_added_event(self):
230 def data_added_event(self):
229 return self.worker.data_added
231 return self.worker.data_added
230
232
231 @property
233 @property
232 def data_added(self):
234 def data_added(self):
233 return self.worker.data_added.is_set()
235 return self.worker.data_added.is_set()
234
236
235 @property
237 @property
236 def reading_paused(self):
238 def reading_paused(self):
237 return not self.worker.keep_reading.is_set()
239 return not self.worker.keep_reading.is_set()
238
240
239 @property
241 @property
240 def done_reading_event(self):
242 def done_reading_event(self):
241 """
243 """
242 Done_reding does not mean that the iterator's buffer is empty.
244 Done_reding does not mean that the iterator's buffer is empty.
243 Iterator might have done reading from underlying source, but the read
245 Iterator might have done reading from underlying source, but the read
244 chunks might still be available for serving through .next() method.
246 chunks might still be available for serving through .next() method.
245
247
246 :returns: An Event class instance.
248 :returns: An Event class instance.
247 """
249 """
248 return self.worker.EOF
250 return self.worker.EOF
249
251
250 @property
252 @property
251 def done_reading(self):
253 def done_reading(self):
252 """
254 """
253 Done_reding does not mean that the iterator's buffer is empty.
255 Done_reding does not mean that the iterator's buffer is empty.
254 Iterator might have done reading from underlying source, but the read
256 Iterator might have done reading from underlying source, but the read
255 chunks might still be available for serving through .next() method.
257 chunks might still be available for serving through .next() method.
256
258
257 :returns: An Bool value.
259 :returns: An Bool value.
258 """
260 """
259 return self.worker.EOF.is_set()
261 return self.worker.EOF.is_set()
260
262
261 @property
263 @property
262 def length(self):
264 def length(self):
263 """
265 """
264 returns int.
266 returns int.
265
267
266 This is the lenght of the que of chunks, not the length of
268 This is the lenght of the que of chunks, not the length of
267 the combined contents in those chunks.
269 the combined contents in those chunks.
268
270
269 __len__() cannot be meaningfully implemented because this
271 __len__() cannot be meaningfully implemented because this
270 reader is just flying throuh a bottomless pit content and
272 reader is just flying throuh a bottomless pit content and
271 can only know the lenght of what it already saw.
273 can only know the lenght of what it already saw.
272
274
273 If __len__() on WSGI server per PEP 3333 returns a value,
275 If __len__() on WSGI server per PEP 3333 returns a value,
274 the responce's length will be set to that. In order not to
276 the responce's length will be set to that. In order not to
275 confuse WSGI PEP3333 servers, we will not implement __len__
277 confuse WSGI PEP3333 servers, we will not implement __len__
276 at all.
278 at all.
277 """
279 """
278 return len(self.data)
280 return len(self.data)
279
281
280 def prepend(self, x):
282 def prepend(self, x):
281 self.data.appendleft(x)
283 self.data.appendleft(x)
282
284
283 def append(self, x):
285 def append(self, x):
284 self.data.append(x)
286 self.data.append(x)
285
287
286 def extend(self, o):
288 def extend(self, o):
287 self.data.extend(o)
289 self.data.extend(o)
288
290
289 def __getitem__(self, i):
291 def __getitem__(self, i):
290 return self.data[i]
292 return self.data[i]
291
293
292
294
293 class SubprocessIOChunker(object):
295 class SubprocessIOChunker(object):
294 """
296 """
295 Processor class wrapping handling of subprocess IO.
297 Processor class wrapping handling of subprocess IO.
296
298
297 .. important::
299 .. important::
298
300
299 Watch out for the method `__del__` on this class. If this object
301 Watch out for the method `__del__` on this class. If this object
300 is deleted, it will kill the subprocess, so avoid to
302 is deleted, it will kill the subprocess, so avoid to
301 return the `output` attribute or usage of it like in the following
303 return the `output` attribute or usage of it like in the following
302 example::
304 example::
303
305
304 # `args` expected to run a program that produces a lot of output
306 # `args` expected to run a program that produces a lot of output
305 output = ''.join(SubprocessIOChunker(
307 output = ''.join(SubprocessIOChunker(
306 args, shell=False, inputstream=inputstream, env=environ).output)
308 args, shell=False, inputstream=inputstream, env=environ).output)
307
309
308 # `output` will not contain all the data, because the __del__ method
310 # `output` will not contain all the data, because the __del__ method
309 # has already killed the subprocess in this case before all output
311 # has already killed the subprocess in this case before all output
310 # has been consumed.
312 # has been consumed.
311
313
312
314
313
315
314 In a way, this is a "communicate()" replacement with a twist.
316 In a way, this is a "communicate()" replacement with a twist.
315
317
316 - We are multithreaded. Writing in and reading out, err are all sep threads.
318 - We are multithreaded. Writing in and reading out, err are all sep threads.
317 - We support concurrent (in and out) stream processing.
319 - We support concurrent (in and out) stream processing.
318 - The output is not a stream. It's a queue of read string (bytes, not unicode)
320 - The output is not a stream. It's a queue of read string (bytes, not unicode)
319 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
321 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
320 - We are non-blocking in more respects than communicate()
322 - We are non-blocking in more respects than communicate()
321 (reading from subprocess out pauses when internal buffer is full, but
323 (reading from subprocess out pauses when internal buffer is full, but
322 does not block the parent calling code. On the flip side, reading from
324 does not block the parent calling code. On the flip side, reading from
323 slow-yielding subprocess may block the iteration until data shows up. This
325 slow-yielding subprocess may block the iteration until data shows up. This
324 does not block the parallel inpipe reading occurring parallel thread.)
326 does not block the parallel inpipe reading occurring parallel thread.)
325
327
326 The purpose of the object is to allow us to wrap subprocess interactions into
328 The purpose of the object is to allow us to wrap subprocess interactions into
327 and interable that can be passed to a WSGI server as the application's return
329 and interable that can be passed to a WSGI server as the application's return
328 value. Because of stream-processing-ability, WSGI does not have to read ALL
330 value. Because of stream-processing-ability, WSGI does not have to read ALL
329 of the subprocess's output and buffer it, before handing it to WSGI server for
331 of the subprocess's output and buffer it, before handing it to WSGI server for
330 HTTP response. Instead, the class initializer reads just a bit of the stream
332 HTTP response. Instead, the class initializer reads just a bit of the stream
331 to figure out if error ocurred or likely to occur and if not, just hands the
333 to figure out if error ocurred or likely to occur and if not, just hands the
332 further iteration over subprocess output to the server for completion of HTTP
334 further iteration over subprocess output to the server for completion of HTTP
333 response.
335 response.
334
336
335 The real or perceived subprocess error is trapped and raised as one of
337 The real or perceived subprocess error is trapped and raised as one of
336 EnvironmentError family of exceptions
338 EnvironmentError family of exceptions
337
339
338 Example usage:
340 Example usage:
339 # try:
341 # try:
340 # answer = SubprocessIOChunker(
342 # answer = SubprocessIOChunker(
341 # cmd,
343 # cmd,
342 # input,
344 # input,
343 # buffer_size = 65536,
345 # buffer_size = 65536,
344 # chunk_size = 4096
346 # chunk_size = 4096
345 # )
347 # )
346 # except (EnvironmentError) as e:
348 # except (EnvironmentError) as e:
347 # print str(e)
349 # print str(e)
348 # raise e
350 # raise e
349 #
351 #
350 # return answer
352 # return answer
351
353
352
354
353 """
355 """
354
356
355 # TODO: johbo: This is used to make sure that the open end of the PIPE
357 # TODO: johbo: This is used to make sure that the open end of the PIPE
356 # is closed in the end. It would be way better to wrap this into an
358 # is closed in the end. It would be way better to wrap this into an
357 # object, so that it is closed automatically once it is consumed or
359 # object, so that it is closed automatically once it is consumed or
358 # something similar.
360 # something similar.
359 _close_input_fd = None
361 _close_input_fd = None
360
362
361 _closed = False
363 _closed = False
362
364
363 def __init__(self, cmd, inputstream=None, buffer_size=65536,
365 def __init__(self, cmd, inputstream=None, buffer_size=65536,
364 chunk_size=4096, starting_values=None, fail_on_stderr=True,
366 chunk_size=4096, starting_values=None, fail_on_stderr=True,
365 fail_on_return_code=True, **kwargs):
367 fail_on_return_code=True, **kwargs):
366 """
368 """
367 Initializes SubprocessIOChunker
369 Initializes SubprocessIOChunker
368
370
369 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
371 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
370 :param inputstream: (Default: None) A file-like, string, or file pointer.
372 :param inputstream: (Default: None) A file-like, string, or file pointer.
371 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
373 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
372 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
374 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
373 :param starting_values: (Default: []) An array of strings to put in front of output que.
375 :param starting_values: (Default: []) An array of strings to put in front of output que.
374 :param fail_on_stderr: (Default: True) Whether to raise an exception in
376 :param fail_on_stderr: (Default: True) Whether to raise an exception in
375 case something is written to stderr.
377 case something is written to stderr.
376 :param fail_on_return_code: (Default: True) Whether to raise an
378 :param fail_on_return_code: (Default: True) Whether to raise an
377 exception if the return code is not 0.
379 exception if the return code is not 0.
378 """
380 """
379
381
380 starting_values = starting_values or []
382 starting_values = starting_values or []
381 if inputstream:
383 if inputstream:
382 input_streamer = StreamFeeder(inputstream)
384 input_streamer = StreamFeeder(inputstream)
383 input_streamer.start()
385 input_streamer.start()
384 inputstream = input_streamer.output
386 inputstream = input_streamer.output
385 self._close_input_fd = inputstream
387 self._close_input_fd = inputstream
386
388
387 self._fail_on_stderr = fail_on_stderr
389 self._fail_on_stderr = fail_on_stderr
388 self._fail_on_return_code = fail_on_return_code
390 self._fail_on_return_code = fail_on_return_code
389
391
390 _shell = kwargs.get('shell', True)
392 _shell = kwargs.get('shell', True)
391 kwargs['shell'] = _shell
393 kwargs['shell'] = _shell
392
394
393 _p = subprocess.Popen(cmd, bufsize=-1,
395 _p = subprocess.Popen(cmd, bufsize=-1,
394 stdin=inputstream,
396 stdin=inputstream,
395 stdout=subprocess.PIPE,
397 stdout=subprocess.PIPE,
396 stderr=subprocess.PIPE,
398 stderr=subprocess.PIPE,
397 **kwargs)
399 **kwargs)
398
400
399 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
401 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
400 starting_values)
402 starting_values)
401 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
403 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
402
404
403 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
405 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
404 # doing this until we reach either end of file, or end of buffer.
406 # doing this until we reach either end of file, or end of buffer.
405 bg_out.data_added_event.wait(1)
407 bg_out.data_added_event.wait(1)
406 bg_out.data_added_event.clear()
408 bg_out.data_added_event.clear()
407
409
408 # at this point it's still ambiguous if we are done reading or just full buffer.
410 # at this point it's still ambiguous if we are done reading or just full buffer.
409 # Either way, if error (returned by ended process, or implied based on
411 # Either way, if error (returned by ended process, or implied based on
410 # presence of stuff in stderr output) we error out.
412 # presence of stuff in stderr output) we error out.
411 # Else, we are happy.
413 # Else, we are happy.
412 _returncode = _p.poll()
414 _returncode = _p.poll()
413
415
414 if ((_returncode and fail_on_return_code) or
416 if ((_returncode and fail_on_return_code) or
415 (fail_on_stderr and _returncode is None and bg_err.length)):
417 (fail_on_stderr and _returncode is None and bg_err.length)):
416 try:
418 try:
417 _p.terminate()
419 _p.terminate()
418 except Exception:
420 except Exception:
419 pass
421 pass
420 bg_out.stop()
422 bg_out.stop()
421 bg_err.stop()
423 bg_err.stop()
422 if fail_on_stderr:
424 if fail_on_stderr:
423 err = ''.join(bg_err)
425 err = ''.join(bg_err)
424 raise EnvironmentError(
426 raise EnvironmentError(
425 "Subprocess exited due to an error:\n" + err)
427 "Subprocess exited due to an error:\n" + err)
426 if _returncode and fail_on_return_code:
428 if _returncode and fail_on_return_code:
427 err = ''.join(bg_err)
429 err = ''.join(bg_err)
428 if not err:
430 if not err:
429 # maybe get empty stderr, try stdout instead
431 # maybe get empty stderr, try stdout instead
430 # in many cases git reports the errors on stdout too
432 # in many cases git reports the errors on stdout too
431 err = ''.join(bg_out)
433 err = ''.join(bg_out)
432 raise EnvironmentError(
434 raise EnvironmentError(
433 "Subprocess exited with non 0 ret code:%s: stderr:%s" % (
435 "Subprocess exited with non 0 ret code:%s: stderr:%s" % (
434 _returncode, err))
436 _returncode, err))
435
437
436 self.process = _p
438 self.process = _p
437 self.output = bg_out
439 self.output = bg_out
438 self.error = bg_err
440 self.error = bg_err
441 self.inputstream = inputstream
439
442
440 def __iter__(self):
443 def __iter__(self):
441 return self
444 return self
442
445
443 def next(self):
446 def next(self):
444 # Note: mikhail: We need to be sure that we are checking the return
447 # Note: mikhail: We need to be sure that we are checking the return
445 # code after the stdout stream is closed. Some processes, e.g. git
448 # code after the stdout stream is closed. Some processes, e.g. git
446 # are doing some magic in between closing stdout and terminating the
449 # are doing some magic in between closing stdout and terminating the
447 # process and, as a result, we are not getting return code on "slow"
450 # process and, as a result, we are not getting return code on "slow"
448 # systems.
451 # systems.
449 result = None
452 result = None
450 stop_iteration = None
453 stop_iteration = None
451 try:
454 try:
452 result = self.output.next()
455 result = self.output.next()
453 except StopIteration as e:
456 except StopIteration as e:
454 stop_iteration = e
457 stop_iteration = e
455
458
456 if self.process.poll() and self._fail_on_return_code:
459 if self.process.poll() and self._fail_on_return_code:
457 err = '%s' % ''.join(self.error)
460 err = '%s' % ''.join(self.error)
458 raise EnvironmentError(
461 raise EnvironmentError(
459 "Subprocess exited due to an error:\n" + err)
462 "Subprocess exited due to an error:\n" + err)
460
463
461 if stop_iteration:
464 if stop_iteration:
462 raise stop_iteration
465 raise stop_iteration
463 return result
466 return result
464
467
465 def throw(self, type, value=None, traceback=None):
468 def throw(self, type, value=None, traceback=None):
466 if self.output.length or not self.output.done_reading:
469 if self.output.length or not self.output.done_reading:
467 raise type(value)
470 raise type(value)
468
471
469 def close(self):
472 def close(self):
470 if self._closed:
473 if self._closed:
471 return
474 return
472 self._closed = True
475 self._closed = True
473 try:
476 try:
474 self.process.terminate()
477 self.process.terminate()
475 except:
478 except:
476 pass
479 pass
477 if self._close_input_fd:
480 if self._close_input_fd:
478 os.close(self._close_input_fd)
481 os.close(self._close_input_fd)
479 try:
482 try:
480 self.output.close()
483 self.output.close()
481 except:
484 except:
482 pass
485 pass
483 try:
486 try:
484 self.error.close()
487 self.error.close()
485 except:
488 except:
486 pass
489 pass
490 try:
491 os.close(self.inputstream)
492 except:
493 pass
487
494
488 def __del__(self):
495 def __del__(self):
489 self.close()
496 self.close()
490
497
491
498
492 def run_command(arguments, env=None):
499 def run_command(arguments, env=None):
493 """
500 """
494 Run the specified command and return the stdout.
501 Run the specified command and return the stdout.
495
502
496 :param arguments: sequence of program arguments (including the program name)
503 :param arguments: sequence of program arguments (including the program name)
497 :type arguments: list[str]
504 :type arguments: list[str]
498 """
505 """
499
506
500 cmd = arguments
507 cmd = arguments
501 log.debug('Running subprocessio command %s', cmd)
508 log.debug('Running subprocessio command %s', cmd)
502 try:
509 try:
503 _opts = {'shell': False, 'fail_on_stderr': False}
510 _opts = {'shell': False, 'fail_on_stderr': False}
504 if env:
511 if env:
505 _opts.update({'env': env})
512 _opts.update({'env': env})
506 p = SubprocessIOChunker(cmd, **_opts)
513 p = SubprocessIOChunker(cmd, **_opts)
507 stdout = ''.join(p)
514 stdout = ''.join(p)
508 stderr = ''.join(''.join(p.error))
515 stderr = ''.join(''.join(p.error))
509 except (EnvironmentError, OSError) as err:
516 except (EnvironmentError, OSError) as err:
510 cmd = ' '.join(cmd) # human friendly CMD
517 cmd = ' '.join(cmd) # human friendly CMD
511 tb_err = ("Couldn't run subprocessio command (%s).\n"
518 tb_err = ("Couldn't run subprocessio command (%s).\n"
512 "Original error was:%s\n" % (cmd, err))
519 "Original error was:%s\n" % (cmd, err))
513 log.exception(tb_err)
520 log.exception(tb_err)
514 raise Exception(tb_err)
521 raise Exception(tb_err)
515
522
516 return stdout, stderr
523 return stdout, stderr
@@ -1,722 +1,732 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 from __future__ import absolute_import
18 from __future__ import absolute_import
19
19
20 import os
20 import os
21 import subprocess
21 import subprocess
22 from urllib2 import URLError
22 from urllib2 import URLError
23 import urlparse
23 import urlparse
24 import logging
24 import logging
25 import posixpath as vcspath
25 import posixpath as vcspath
26 import StringIO
26 import StringIO
27 import urllib
27 import urllib
28 import traceback
28 import traceback
29
29
30 import svn.client
30 import svn.client
31 import svn.core
31 import svn.core
32 import svn.delta
32 import svn.delta
33 import svn.diff
33 import svn.diff
34 import svn.fs
34 import svn.fs
35 import svn.repos
35 import svn.repos
36
36
37 from vcsserver import svn_diff, exceptions, subprocessio, settings
37 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 from vcsserver.base import RepoFactory, raise_from_original
38 from vcsserver.base import RepoFactory, raise_from_original
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 # Set of svn compatible version flags.
43 # Set of svn compatible version flags.
44 # Compare with subversion/svnadmin/svnadmin.c
44 # Compare with subversion/svnadmin/svnadmin.c
45 svn_compatible_versions = {
45 svn_compatible_versions = {
46 'pre-1.4-compatible',
46 'pre-1.4-compatible',
47 'pre-1.5-compatible',
47 'pre-1.5-compatible',
48 'pre-1.6-compatible',
48 'pre-1.6-compatible',
49 'pre-1.8-compatible',
49 'pre-1.8-compatible',
50 'pre-1.9-compatible'
50 'pre-1.9-compatible'
51 }
51 }
52
52
53 svn_compatible_versions_map = {
53 svn_compatible_versions_map = {
54 'pre-1.4-compatible': '1.3',
54 'pre-1.4-compatible': '1.3',
55 'pre-1.5-compatible': '1.4',
55 'pre-1.5-compatible': '1.4',
56 'pre-1.6-compatible': '1.5',
56 'pre-1.6-compatible': '1.5',
57 'pre-1.8-compatible': '1.7',
57 'pre-1.8-compatible': '1.7',
58 'pre-1.9-compatible': '1.8',
58 'pre-1.9-compatible': '1.8',
59 }
59 }
60
60
61
61
62 def reraise_safe_exceptions(func):
62 def reraise_safe_exceptions(func):
63 """Decorator for converting svn exceptions to something neutral."""
63 """Decorator for converting svn exceptions to something neutral."""
64 def wrapper(*args, **kwargs):
64 def wrapper(*args, **kwargs):
65 try:
65 try:
66 return func(*args, **kwargs)
66 return func(*args, **kwargs)
67 except Exception as e:
67 except Exception as e:
68 if not hasattr(e, '_vcs_kind'):
68 if not hasattr(e, '_vcs_kind'):
69 log.exception("Unhandled exception in svn remote call")
69 log.exception("Unhandled exception in svn remote call")
70 raise_from_original(exceptions.UnhandledException(e))
70 raise_from_original(exceptions.UnhandledException(e))
71 raise
71 raise
72 return wrapper
72 return wrapper
73
73
74
74
75 class SubversionFactory(RepoFactory):
75 class SubversionFactory(RepoFactory):
76 repo_type = 'svn'
76 repo_type = 'svn'
77
77
78 def _create_repo(self, wire, create, compatible_version):
78 def _create_repo(self, wire, create, compatible_version):
79 path = svn.core.svn_path_canonicalize(wire['path'])
79 path = svn.core.svn_path_canonicalize(wire['path'])
80 if create:
80 if create:
81 fs_config = {'compatible-version': '1.9'}
81 fs_config = {'compatible-version': '1.9'}
82 if compatible_version:
82 if compatible_version:
83 if compatible_version not in svn_compatible_versions:
83 if compatible_version not in svn_compatible_versions:
84 raise Exception('Unknown SVN compatible version "{}"'
84 raise Exception('Unknown SVN compatible version "{}"'
85 .format(compatible_version))
85 .format(compatible_version))
86 fs_config['compatible-version'] = \
86 fs_config['compatible-version'] = \
87 svn_compatible_versions_map[compatible_version]
87 svn_compatible_versions_map[compatible_version]
88
88
89 log.debug('Create SVN repo with config "%s"', fs_config)
89 log.debug('Create SVN repo with config "%s"', fs_config)
90 repo = svn.repos.create(path, "", "", None, fs_config)
90 repo = svn.repos.create(path, "", "", None, fs_config)
91 else:
91 else:
92 repo = svn.repos.open(path)
92 repo = svn.repos.open(path)
93
93
94 log.debug('Got SVN object: %s', repo)
94 log.debug('Got SVN object: %s', repo)
95 return repo
95 return repo
96
96
97 def repo(self, wire, create=False, compatible_version=None):
97 def repo(self, wire, create=False, compatible_version=None):
98 """
98 """
99 Get a repository instance for the given path.
99 Get a repository instance for the given path.
100
100
101 Uses internally the low level beaker API since the decorators introduce
101 Uses internally the low level beaker API since the decorators introduce
102 significant overhead.
102 significant overhead.
103 """
103 """
104 region = self._cache_region
104 region = self._cache_region
105 context = wire.get('context', None)
105 context = wire.get('context', None)
106 repo_path = wire.get('path', '')
106 repo_path = wire.get('path', '')
107 context_uid = '{}'.format(context)
107 context_uid = '{}'.format(context)
108 cache = wire.get('cache', True)
108 cache = wire.get('cache', True)
109 cache_on = context and cache
109 cache_on = context and cache
110
110
111 @region.conditional_cache_on_arguments(condition=cache_on)
111 @region.conditional_cache_on_arguments(condition=cache_on)
112 def create_new_repo(_repo_type, _repo_path, _context_uid, compatible_version_id):
112 def create_new_repo(_repo_type, _repo_path, _context_uid, compatible_version_id):
113 return self._create_repo(wire, create, compatible_version)
113 return self._create_repo(wire, create, compatible_version)
114
114
115 return create_new_repo(self.repo_type, repo_path, context_uid,
115 return create_new_repo(self.repo_type, repo_path, context_uid,
116 compatible_version)
116 compatible_version)
117
117
118
118
119 NODE_TYPE_MAPPING = {
119 NODE_TYPE_MAPPING = {
120 svn.core.svn_node_file: 'file',
120 svn.core.svn_node_file: 'file',
121 svn.core.svn_node_dir: 'dir',
121 svn.core.svn_node_dir: 'dir',
122 }
122 }
123
123
124
124
125 class SvnRemote(object):
125 class SvnRemote(object):
126
126
127 def __init__(self, factory, hg_factory=None):
127 def __init__(self, factory, hg_factory=None):
128 self._factory = factory
128 self._factory = factory
129 # TODO: Remove once we do not use internal Mercurial objects anymore
129 # TODO: Remove once we do not use internal Mercurial objects anymore
130 # for subversion
130 # for subversion
131 self._hg_factory = hg_factory
131 self._hg_factory = hg_factory
132
132
133 @reraise_safe_exceptions
133 @reraise_safe_exceptions
134 def discover_svn_version(self):
134 def discover_svn_version(self):
135 try:
135 try:
136 import svn.core
136 import svn.core
137 svn_ver = svn.core.SVN_VERSION
137 svn_ver = svn.core.SVN_VERSION
138 except ImportError:
138 except ImportError:
139 svn_ver = None
139 svn_ver = None
140 return svn_ver
140 return svn_ver
141
141
142 def check_url(self, url, config_items):
142 def check_url(self, url, config_items):
143 # this can throw exception if not installed, but we detect this
143 # this can throw exception if not installed, but we detect this
144 from hgsubversion import svnrepo
144 from hgsubversion import svnrepo
145
145
146 baseui = self._hg_factory._create_config(config_items)
146 baseui = self._hg_factory._create_config(config_items)
147 # uuid function get's only valid UUID from proper repo, else
147 # uuid function get's only valid UUID from proper repo, else
148 # throws exception
148 # throws exception
149 try:
149 try:
150 svnrepo.svnremoterepo(baseui, url).svn.uuid
150 svnrepo.svnremoterepo(baseui, url).svn.uuid
151 except Exception:
151 except Exception:
152 tb = traceback.format_exc()
152 tb = traceback.format_exc()
153 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
153 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
154 raise URLError(
154 raise URLError(
155 '"%s" is not a valid Subversion source url.' % (url, ))
155 '"%s" is not a valid Subversion source url.' % (url, ))
156 return True
156 return True
157
157
158 def is_path_valid_repository(self, wire, path):
158 def is_path_valid_repository(self, wire, path):
159
159
160 # NOTE(marcink): short circuit the check for SVN repo
160 # NOTE(marcink): short circuit the check for SVN repo
161 # the repos.open might be expensive to check, but we have one cheap
161 # the repos.open might be expensive to check, but we have one cheap
162 # pre condition that we can use, to check for 'format' file
162 # pre condition that we can use, to check for 'format' file
163
163
164 if not os.path.isfile(os.path.join(path, 'format')):
164 if not os.path.isfile(os.path.join(path, 'format')):
165 return False
165 return False
166
166
167 try:
167 try:
168 svn.repos.open(path)
168 svn.repos.open(path)
169 except svn.core.SubversionException:
169 except svn.core.SubversionException:
170 tb = traceback.format_exc()
170 tb = traceback.format_exc()
171 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
171 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
172 return False
172 return False
173 return True
173 return True
174
174
175 @reraise_safe_exceptions
175 @reraise_safe_exceptions
176 def verify(self, wire,):
176 def verify(self, wire,):
177 repo_path = wire['path']
177 repo_path = wire['path']
178 if not self.is_path_valid_repository(wire, repo_path):
178 if not self.is_path_valid_repository(wire, repo_path):
179 raise Exception(
179 raise Exception(
180 "Path %s is not a valid Subversion repository." % repo_path)
180 "Path %s is not a valid Subversion repository." % repo_path)
181
181
182 cmd = ['svnadmin', 'info', repo_path]
182 cmd = ['svnadmin', 'info', repo_path]
183 stdout, stderr = subprocessio.run_command(cmd)
183 stdout, stderr = subprocessio.run_command(cmd)
184 return stdout
184 return stdout
185
185
186 def lookup(self, wire, revision):
186 def lookup(self, wire, revision):
187 if revision not in [-1, None, 'HEAD']:
187 if revision not in [-1, None, 'HEAD']:
188 raise NotImplementedError
188 raise NotImplementedError
189 repo = self._factory.repo(wire)
189 repo = self._factory.repo(wire)
190 fs_ptr = svn.repos.fs(repo)
190 fs_ptr = svn.repos.fs(repo)
191 head = svn.fs.youngest_rev(fs_ptr)
191 head = svn.fs.youngest_rev(fs_ptr)
192 return head
192 return head
193
193
194 def lookup_interval(self, wire, start_ts, end_ts):
194 def lookup_interval(self, wire, start_ts, end_ts):
195 repo = self._factory.repo(wire)
195 repo = self._factory.repo(wire)
196 fsobj = svn.repos.fs(repo)
196 fsobj = svn.repos.fs(repo)
197 start_rev = None
197 start_rev = None
198 end_rev = None
198 end_rev = None
199 if start_ts:
199 if start_ts:
200 start_ts_svn = apr_time_t(start_ts)
200 start_ts_svn = apr_time_t(start_ts)
201 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
201 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
202 else:
202 else:
203 start_rev = 1
203 start_rev = 1
204 if end_ts:
204 if end_ts:
205 end_ts_svn = apr_time_t(end_ts)
205 end_ts_svn = apr_time_t(end_ts)
206 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
206 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
207 else:
207 else:
208 end_rev = svn.fs.youngest_rev(fsobj)
208 end_rev = svn.fs.youngest_rev(fsobj)
209 return start_rev, end_rev
209 return start_rev, end_rev
210
210
211 def revision_properties(self, wire, revision):
211 def revision_properties(self, wire, revision):
212 repo = self._factory.repo(wire)
212 repo = self._factory.repo(wire)
213 fs_ptr = svn.repos.fs(repo)
213 fs_ptr = svn.repos.fs(repo)
214 return svn.fs.revision_proplist(fs_ptr, revision)
214 return svn.fs.revision_proplist(fs_ptr, revision)
215
215
216 def revision_changes(self, wire, revision):
216 def revision_changes(self, wire, revision):
217
217
218 repo = self._factory.repo(wire)
218 repo = self._factory.repo(wire)
219 fsobj = svn.repos.fs(repo)
219 fsobj = svn.repos.fs(repo)
220 rev_root = svn.fs.revision_root(fsobj, revision)
220 rev_root = svn.fs.revision_root(fsobj, revision)
221
221
222 editor = svn.repos.ChangeCollector(fsobj, rev_root)
222 editor = svn.repos.ChangeCollector(fsobj, rev_root)
223 editor_ptr, editor_baton = svn.delta.make_editor(editor)
223 editor_ptr, editor_baton = svn.delta.make_editor(editor)
224 base_dir = ""
224 base_dir = ""
225 send_deltas = False
225 send_deltas = False
226 svn.repos.replay2(
226 svn.repos.replay2(
227 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
227 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
228 editor_ptr, editor_baton, None)
228 editor_ptr, editor_baton, None)
229
229
230 added = []
230 added = []
231 changed = []
231 changed = []
232 removed = []
232 removed = []
233
233
234 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
234 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
235 for path, change in editor.changes.iteritems():
235 for path, change in editor.changes.iteritems():
236 # TODO: Decide what to do with directory nodes. Subversion can add
236 # TODO: Decide what to do with directory nodes. Subversion can add
237 # empty directories.
237 # empty directories.
238
238
239 if change.item_kind == svn.core.svn_node_dir:
239 if change.item_kind == svn.core.svn_node_dir:
240 continue
240 continue
241 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
241 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
242 added.append(path)
242 added.append(path)
243 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
243 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
244 svn.repos.CHANGE_ACTION_REPLACE]:
244 svn.repos.CHANGE_ACTION_REPLACE]:
245 changed.append(path)
245 changed.append(path)
246 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
246 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
247 removed.append(path)
247 removed.append(path)
248 else:
248 else:
249 raise NotImplementedError(
249 raise NotImplementedError(
250 "Action %s not supported on path %s" % (
250 "Action %s not supported on path %s" % (
251 change.action, path))
251 change.action, path))
252
252
253 changes = {
253 changes = {
254 'added': added,
254 'added': added,
255 'changed': changed,
255 'changed': changed,
256 'removed': removed,
256 'removed': removed,
257 }
257 }
258 return changes
258 return changes
259
259
260 def node_history(self, wire, path, revision, limit):
260 def node_history(self, wire, path, revision, limit):
261 cross_copies = False
261 cross_copies = False
262 repo = self._factory.repo(wire)
262 repo = self._factory.repo(wire)
263 fsobj = svn.repos.fs(repo)
263 fsobj = svn.repos.fs(repo)
264 rev_root = svn.fs.revision_root(fsobj, revision)
264 rev_root = svn.fs.revision_root(fsobj, revision)
265
265
266 history_revisions = []
266 history_revisions = []
267 history = svn.fs.node_history(rev_root, path)
267 history = svn.fs.node_history(rev_root, path)
268 history = svn.fs.history_prev(history, cross_copies)
268 history = svn.fs.history_prev(history, cross_copies)
269 while history:
269 while history:
270 __, node_revision = svn.fs.history_location(history)
270 __, node_revision = svn.fs.history_location(history)
271 history_revisions.append(node_revision)
271 history_revisions.append(node_revision)
272 if limit and len(history_revisions) >= limit:
272 if limit and len(history_revisions) >= limit:
273 break
273 break
274 history = svn.fs.history_prev(history, cross_copies)
274 history = svn.fs.history_prev(history, cross_copies)
275 return history_revisions
275 return history_revisions
276
276
277 def node_properties(self, wire, path, revision):
277 def node_properties(self, wire, path, revision):
278 repo = self._factory.repo(wire)
278 repo = self._factory.repo(wire)
279 fsobj = svn.repos.fs(repo)
279 fsobj = svn.repos.fs(repo)
280 rev_root = svn.fs.revision_root(fsobj, revision)
280 rev_root = svn.fs.revision_root(fsobj, revision)
281 return svn.fs.node_proplist(rev_root, path)
281 return svn.fs.node_proplist(rev_root, path)
282
282
283 def file_annotate(self, wire, path, revision):
283 def file_annotate(self, wire, path, revision):
284 abs_path = 'file://' + urllib.pathname2url(
284 abs_path = 'file://' + urllib.pathname2url(
285 vcspath.join(wire['path'], path))
285 vcspath.join(wire['path'], path))
286 file_uri = svn.core.svn_path_canonicalize(abs_path)
286 file_uri = svn.core.svn_path_canonicalize(abs_path)
287
287
288 start_rev = svn_opt_revision_value_t(0)
288 start_rev = svn_opt_revision_value_t(0)
289 peg_rev = svn_opt_revision_value_t(revision)
289 peg_rev = svn_opt_revision_value_t(revision)
290 end_rev = peg_rev
290 end_rev = peg_rev
291
291
292 annotations = []
292 annotations = []
293
293
294 def receiver(line_no, revision, author, date, line, pool):
294 def receiver(line_no, revision, author, date, line, pool):
295 annotations.append((line_no, revision, line))
295 annotations.append((line_no, revision, line))
296
296
297 # TODO: Cannot use blame5, missing typemap function in the swig code
297 # TODO: Cannot use blame5, missing typemap function in the swig code
298 try:
298 try:
299 svn.client.blame2(
299 svn.client.blame2(
300 file_uri, peg_rev, start_rev, end_rev,
300 file_uri, peg_rev, start_rev, end_rev,
301 receiver, svn.client.create_context())
301 receiver, svn.client.create_context())
302 except svn.core.SubversionException as exc:
302 except svn.core.SubversionException as exc:
303 log.exception("Error during blame operation.")
303 log.exception("Error during blame operation.")
304 raise Exception(
304 raise Exception(
305 "Blame not supported or file does not exist at path %s. "
305 "Blame not supported or file does not exist at path %s. "
306 "Error %s." % (path, exc))
306 "Error %s." % (path, exc))
307
307
308 return annotations
308 return annotations
309
309
310 def get_node_type(self, wire, path, rev=None):
310 def get_node_type(self, wire, path, rev=None):
311 repo = self._factory.repo(wire)
311 repo = self._factory.repo(wire)
312 fs_ptr = svn.repos.fs(repo)
312 fs_ptr = svn.repos.fs(repo)
313 if rev is None:
313 if rev is None:
314 rev = svn.fs.youngest_rev(fs_ptr)
314 rev = svn.fs.youngest_rev(fs_ptr)
315 root = svn.fs.revision_root(fs_ptr, rev)
315 root = svn.fs.revision_root(fs_ptr, rev)
316 node = svn.fs.check_path(root, path)
316 node = svn.fs.check_path(root, path)
317 return NODE_TYPE_MAPPING.get(node, None)
317 return NODE_TYPE_MAPPING.get(node, None)
318
318
319 def get_nodes(self, wire, path, revision=None):
319 def get_nodes(self, wire, path, revision=None):
320 repo = self._factory.repo(wire)
320 repo = self._factory.repo(wire)
321 fsobj = svn.repos.fs(repo)
321 fsobj = svn.repos.fs(repo)
322 if revision is None:
322 if revision is None:
323 revision = svn.fs.youngest_rev(fsobj)
323 revision = svn.fs.youngest_rev(fsobj)
324 root = svn.fs.revision_root(fsobj, revision)
324 root = svn.fs.revision_root(fsobj, revision)
325 entries = svn.fs.dir_entries(root, path)
325 entries = svn.fs.dir_entries(root, path)
326 result = []
326 result = []
327 for entry_path, entry_info in entries.iteritems():
327 for entry_path, entry_info in entries.iteritems():
328 result.append(
328 result.append(
329 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
329 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
330 return result
330 return result
331
331
332 def get_file_content(self, wire, path, rev=None):
332 def get_file_content(self, wire, path, rev=None):
333 repo = self._factory.repo(wire)
333 repo = self._factory.repo(wire)
334 fsobj = svn.repos.fs(repo)
334 fsobj = svn.repos.fs(repo)
335 if rev is None:
335 if rev is None:
336 rev = svn.fs.youngest_revision(fsobj)
336 rev = svn.fs.youngest_revision(fsobj)
337 root = svn.fs.revision_root(fsobj, rev)
337 root = svn.fs.revision_root(fsobj, rev)
338 content = svn.core.Stream(svn.fs.file_contents(root, path))
338 content = svn.core.Stream(svn.fs.file_contents(root, path))
339 return content.read()
339 return content.read()
340
340
341 def get_file_size(self, wire, path, revision=None):
341 def get_file_size(self, wire, path, revision=None):
342 repo = self._factory.repo(wire)
342 repo = self._factory.repo(wire)
343 fsobj = svn.repos.fs(repo)
343 fsobj = svn.repos.fs(repo)
344 if revision is None:
344 if revision is None:
345 revision = svn.fs.youngest_revision(fsobj)
345 revision = svn.fs.youngest_revision(fsobj)
346 root = svn.fs.revision_root(fsobj, revision)
346 root = svn.fs.revision_root(fsobj, revision)
347 size = svn.fs.file_length(root, path)
347 size = svn.fs.file_length(root, path)
348 return size
348 return size
349
349
350 def create_repository(self, wire, compatible_version=None):
350 def create_repository(self, wire, compatible_version=None):
351 log.info('Creating Subversion repository in path "%s"', wire['path'])
351 log.info('Creating Subversion repository in path "%s"', wire['path'])
352 self._factory.repo(wire, create=True,
352 self._factory.repo(wire, create=True,
353 compatible_version=compatible_version)
353 compatible_version=compatible_version)
354
354
355 def get_url_and_credentials(self, src_url):
355 def get_url_and_credentials(self, src_url):
356 obj = urlparse.urlparse(src_url)
356 obj = urlparse.urlparse(src_url)
357 username = obj.username or None
357 username = obj.username or None
358 password = obj.password or None
358 password = obj.password or None
359 return username, password, src_url
359 return username, password, src_url
360
360
361 def import_remote_repository(self, wire, src_url):
361 def import_remote_repository(self, wire, src_url):
362 repo_path = wire['path']
362 repo_path = wire['path']
363 if not self.is_path_valid_repository(wire, repo_path):
363 if not self.is_path_valid_repository(wire, repo_path):
364 raise Exception(
364 raise Exception(
365 "Path %s is not a valid Subversion repository." % repo_path)
365 "Path %s is not a valid Subversion repository." % repo_path)
366
366
367 username, password, src_url = self.get_url_and_credentials(src_url)
367 username, password, src_url = self.get_url_and_credentials(src_url)
368 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
368 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
369 '--trust-server-cert-failures=unknown-ca']
369 '--trust-server-cert-failures=unknown-ca']
370 if username and password:
370 if username and password:
371 rdump_cmd += ['--username', username, '--password', password]
371 rdump_cmd += ['--username', username, '--password', password]
372 rdump_cmd += [src_url]
372 rdump_cmd += [src_url]
373
373
374 rdump = subprocess.Popen(
374 rdump = subprocess.Popen(
375 rdump_cmd,
375 rdump_cmd,
376 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
376 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
377 load = subprocess.Popen(
377 load = subprocess.Popen(
378 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
378 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
379
379
380 # TODO: johbo: This can be a very long operation, might be better
380 # TODO: johbo: This can be a very long operation, might be better
381 # to track some kind of status and provide an api to check if the
381 # to track some kind of status and provide an api to check if the
382 # import is done.
382 # import is done.
383 rdump.wait()
383 rdump.wait()
384 load.wait()
384 load.wait()
385
385
386 log.debug('Return process ended with code: %s', rdump.returncode)
386 log.debug('Return process ended with code: %s', rdump.returncode)
387 if rdump.returncode != 0:
387 if rdump.returncode != 0:
388 errors = rdump.stderr.read()
388 errors = rdump.stderr.read()
389 log.error('svnrdump dump failed: statuscode %s: message: %s',
389 log.error('svnrdump dump failed: statuscode %s: message: %s',
390 rdump.returncode, errors)
390 rdump.returncode, errors)
391 reason = 'UNKNOWN'
391 reason = 'UNKNOWN'
392 if 'svnrdump: E230001:' in errors:
392 if 'svnrdump: E230001:' in errors:
393 reason = 'INVALID_CERTIFICATE'
393 reason = 'INVALID_CERTIFICATE'
394
394
395 if reason == 'UNKNOWN':
395 if reason == 'UNKNOWN':
396 reason = 'UNKNOWN:{}'.format(errors)
396 reason = 'UNKNOWN:{}'.format(errors)
397 raise Exception(
397 raise Exception(
398 'Failed to dump the remote repository from %s. Reason:%s' % (
398 'Failed to dump the remote repository from %s. Reason:%s' % (
399 src_url, reason))
399 src_url, reason))
400 if load.returncode != 0:
400 if load.returncode != 0:
401 raise Exception(
401 raise Exception(
402 'Failed to load the dump of remote repository from %s.' %
402 'Failed to load the dump of remote repository from %s.' %
403 (src_url, ))
403 (src_url, ))
404
404
405 def commit(self, wire, message, author, timestamp, updated, removed):
405 def commit(self, wire, message, author, timestamp, updated, removed):
406 assert isinstance(message, str)
406 assert isinstance(message, str)
407 assert isinstance(author, str)
407 assert isinstance(author, str)
408
408
409 repo = self._factory.repo(wire)
409 repo = self._factory.repo(wire)
410 fsobj = svn.repos.fs(repo)
410 fsobj = svn.repos.fs(repo)
411
411
412 rev = svn.fs.youngest_rev(fsobj)
412 rev = svn.fs.youngest_rev(fsobj)
413 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
413 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
414 txn_root = svn.fs.txn_root(txn)
414 txn_root = svn.fs.txn_root(txn)
415
415
416 for node in updated:
416 for node in updated:
417 TxnNodeProcessor(node, txn_root).update()
417 TxnNodeProcessor(node, txn_root).update()
418 for node in removed:
418 for node in removed:
419 TxnNodeProcessor(node, txn_root).remove()
419 TxnNodeProcessor(node, txn_root).remove()
420
420
421 commit_id = svn.repos.fs_commit_txn(repo, txn)
421 commit_id = svn.repos.fs_commit_txn(repo, txn)
422
422
423 if timestamp:
423 if timestamp:
424 apr_time = apr_time_t(timestamp)
424 apr_time = apr_time_t(timestamp)
425 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
425 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
426 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
426 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
427
427
428 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
428 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
429 return commit_id
429 return commit_id
430
430
431 def diff(self, wire, rev1, rev2, path1=None, path2=None,
431 def diff(self, wire, rev1, rev2, path1=None, path2=None,
432 ignore_whitespace=False, context=3):
432 ignore_whitespace=False, context=3):
433
433
434 wire.update(cache=False)
434 wire.update(cache=False)
435 repo = self._factory.repo(wire)
435 repo = self._factory.repo(wire)
436 diff_creator = SvnDiffer(
436 diff_creator = SvnDiffer(
437 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
437 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
438 try:
438 try:
439 return diff_creator.generate_diff()
439 return diff_creator.generate_diff()
440 except svn.core.SubversionException as e:
440 except svn.core.SubversionException as e:
441 log.exception(
441 log.exception(
442 "Error during diff operation operation. "
442 "Error during diff operation operation. "
443 "Path might not exist %s, %s" % (path1, path2))
443 "Path might not exist %s, %s" % (path1, path2))
444 return ""
444 return ""
445
445
446 @reraise_safe_exceptions
446 @reraise_safe_exceptions
447 def is_large_file(self, wire, path):
447 def is_large_file(self, wire, path):
448 return False
448 return False
449
449
450 @reraise_safe_exceptions
450 @reraise_safe_exceptions
451 def install_hooks(self, wire, force=False):
451 def install_hooks(self, wire, force=False):
452 from vcsserver.hook_utils import install_svn_hooks
452 from vcsserver.hook_utils import install_svn_hooks
453 repo_path = wire['path']
453 repo_path = wire['path']
454 binary_dir = settings.BINARY_DIR
454 binary_dir = settings.BINARY_DIR
455 executable = None
455 executable = None
456 if binary_dir:
456 if binary_dir:
457 executable = os.path.join(binary_dir, 'python')
457 executable = os.path.join(binary_dir, 'python')
458 return install_svn_hooks(
458 return install_svn_hooks(
459 repo_path, executable=executable, force_create=force)
459 repo_path, executable=executable, force_create=force)
460
460
461 @reraise_safe_exceptions
462 def get_hooks_info(self, wire):
463 from vcsserver.hook_utils import (
464 get_svn_pre_hook_version, get_svn_post_hook_version)
465 repo_path = wire['path']
466 return {
467 'pre_version': get_svn_pre_hook_version(repo_path),
468 'post_version': get_svn_post_hook_version(repo_path),
469 }
470
461
471
462 class SvnDiffer(object):
472 class SvnDiffer(object):
463 """
473 """
464 Utility to create diffs based on difflib and the Subversion api
474 Utility to create diffs based on difflib and the Subversion api
465 """
475 """
466
476
467 binary_content = False
477 binary_content = False
468
478
469 def __init__(
479 def __init__(
470 self, repo, src_rev, src_path, tgt_rev, tgt_path,
480 self, repo, src_rev, src_path, tgt_rev, tgt_path,
471 ignore_whitespace, context):
481 ignore_whitespace, context):
472 self.repo = repo
482 self.repo = repo
473 self.ignore_whitespace = ignore_whitespace
483 self.ignore_whitespace = ignore_whitespace
474 self.context = context
484 self.context = context
475
485
476 fsobj = svn.repos.fs(repo)
486 fsobj = svn.repos.fs(repo)
477
487
478 self.tgt_rev = tgt_rev
488 self.tgt_rev = tgt_rev
479 self.tgt_path = tgt_path or ''
489 self.tgt_path = tgt_path or ''
480 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
490 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
481 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
491 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
482
492
483 self.src_rev = src_rev
493 self.src_rev = src_rev
484 self.src_path = src_path or self.tgt_path
494 self.src_path = src_path or self.tgt_path
485 self.src_root = svn.fs.revision_root(fsobj, src_rev)
495 self.src_root = svn.fs.revision_root(fsobj, src_rev)
486 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
496 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
487
497
488 self._validate()
498 self._validate()
489
499
490 def _validate(self):
500 def _validate(self):
491 if (self.tgt_kind != svn.core.svn_node_none and
501 if (self.tgt_kind != svn.core.svn_node_none and
492 self.src_kind != svn.core.svn_node_none and
502 self.src_kind != svn.core.svn_node_none and
493 self.src_kind != self.tgt_kind):
503 self.src_kind != self.tgt_kind):
494 # TODO: johbo: proper error handling
504 # TODO: johbo: proper error handling
495 raise Exception(
505 raise Exception(
496 "Source and target are not compatible for diff generation. "
506 "Source and target are not compatible for diff generation. "
497 "Source type: %s, target type: %s" %
507 "Source type: %s, target type: %s" %
498 (self.src_kind, self.tgt_kind))
508 (self.src_kind, self.tgt_kind))
499
509
500 def generate_diff(self):
510 def generate_diff(self):
501 buf = StringIO.StringIO()
511 buf = StringIO.StringIO()
502 if self.tgt_kind == svn.core.svn_node_dir:
512 if self.tgt_kind == svn.core.svn_node_dir:
503 self._generate_dir_diff(buf)
513 self._generate_dir_diff(buf)
504 else:
514 else:
505 self._generate_file_diff(buf)
515 self._generate_file_diff(buf)
506 return buf.getvalue()
516 return buf.getvalue()
507
517
508 def _generate_dir_diff(self, buf):
518 def _generate_dir_diff(self, buf):
509 editor = DiffChangeEditor()
519 editor = DiffChangeEditor()
510 editor_ptr, editor_baton = svn.delta.make_editor(editor)
520 editor_ptr, editor_baton = svn.delta.make_editor(editor)
511 svn.repos.dir_delta2(
521 svn.repos.dir_delta2(
512 self.src_root,
522 self.src_root,
513 self.src_path,
523 self.src_path,
514 '', # src_entry
524 '', # src_entry
515 self.tgt_root,
525 self.tgt_root,
516 self.tgt_path,
526 self.tgt_path,
517 editor_ptr, editor_baton,
527 editor_ptr, editor_baton,
518 authorization_callback_allow_all,
528 authorization_callback_allow_all,
519 False, # text_deltas
529 False, # text_deltas
520 svn.core.svn_depth_infinity, # depth
530 svn.core.svn_depth_infinity, # depth
521 False, # entry_props
531 False, # entry_props
522 False, # ignore_ancestry
532 False, # ignore_ancestry
523 )
533 )
524
534
525 for path, __, change in sorted(editor.changes):
535 for path, __, change in sorted(editor.changes):
526 self._generate_node_diff(
536 self._generate_node_diff(
527 buf, change, path, self.tgt_path, path, self.src_path)
537 buf, change, path, self.tgt_path, path, self.src_path)
528
538
529 def _generate_file_diff(self, buf):
539 def _generate_file_diff(self, buf):
530 change = None
540 change = None
531 if self.src_kind == svn.core.svn_node_none:
541 if self.src_kind == svn.core.svn_node_none:
532 change = "add"
542 change = "add"
533 elif self.tgt_kind == svn.core.svn_node_none:
543 elif self.tgt_kind == svn.core.svn_node_none:
534 change = "delete"
544 change = "delete"
535 tgt_base, tgt_path = vcspath.split(self.tgt_path)
545 tgt_base, tgt_path = vcspath.split(self.tgt_path)
536 src_base, src_path = vcspath.split(self.src_path)
546 src_base, src_path = vcspath.split(self.src_path)
537 self._generate_node_diff(
547 self._generate_node_diff(
538 buf, change, tgt_path, tgt_base, src_path, src_base)
548 buf, change, tgt_path, tgt_base, src_path, src_base)
539
549
540 def _generate_node_diff(
550 def _generate_node_diff(
541 self, buf, change, tgt_path, tgt_base, src_path, src_base):
551 self, buf, change, tgt_path, tgt_base, src_path, src_base):
542
552
543 if self.src_rev == self.tgt_rev and tgt_base == src_base:
553 if self.src_rev == self.tgt_rev and tgt_base == src_base:
544 # makes consistent behaviour with git/hg to return empty diff if
554 # makes consistent behaviour with git/hg to return empty diff if
545 # we compare same revisions
555 # we compare same revisions
546 return
556 return
547
557
548 tgt_full_path = vcspath.join(tgt_base, tgt_path)
558 tgt_full_path = vcspath.join(tgt_base, tgt_path)
549 src_full_path = vcspath.join(src_base, src_path)
559 src_full_path = vcspath.join(src_base, src_path)
550
560
551 self.binary_content = False
561 self.binary_content = False
552 mime_type = self._get_mime_type(tgt_full_path)
562 mime_type = self._get_mime_type(tgt_full_path)
553
563
554 if mime_type and not mime_type.startswith('text'):
564 if mime_type and not mime_type.startswith('text'):
555 self.binary_content = True
565 self.binary_content = True
556 buf.write("=" * 67 + '\n')
566 buf.write("=" * 67 + '\n')
557 buf.write("Cannot display: file marked as a binary type.\n")
567 buf.write("Cannot display: file marked as a binary type.\n")
558 buf.write("svn:mime-type = %s\n" % mime_type)
568 buf.write("svn:mime-type = %s\n" % mime_type)
559 buf.write("Index: %s\n" % (tgt_path, ))
569 buf.write("Index: %s\n" % (tgt_path, ))
560 buf.write("=" * 67 + '\n')
570 buf.write("=" * 67 + '\n')
561 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
571 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
562 'tgt_path': tgt_path})
572 'tgt_path': tgt_path})
563
573
564 if change == 'add':
574 if change == 'add':
565 # TODO: johbo: SVN is missing a zero here compared to git
575 # TODO: johbo: SVN is missing a zero here compared to git
566 buf.write("new file mode 10644\n")
576 buf.write("new file mode 10644\n")
567
577
568 #TODO(marcink): intro to binary detection of svn patches
578 #TODO(marcink): intro to binary detection of svn patches
569 # if self.binary_content:
579 # if self.binary_content:
570 # buf.write('GIT binary patch\n')
580 # buf.write('GIT binary patch\n')
571
581
572 buf.write("--- /dev/null\t(revision 0)\n")
582 buf.write("--- /dev/null\t(revision 0)\n")
573 src_lines = []
583 src_lines = []
574 else:
584 else:
575 if change == 'delete':
585 if change == 'delete':
576 buf.write("deleted file mode 10644\n")
586 buf.write("deleted file mode 10644\n")
577
587
578 #TODO(marcink): intro to binary detection of svn patches
588 #TODO(marcink): intro to binary detection of svn patches
579 # if self.binary_content:
589 # if self.binary_content:
580 # buf.write('GIT binary patch\n')
590 # buf.write('GIT binary patch\n')
581
591
582 buf.write("--- a/%s\t(revision %s)\n" % (
592 buf.write("--- a/%s\t(revision %s)\n" % (
583 src_path, self.src_rev))
593 src_path, self.src_rev))
584 src_lines = self._svn_readlines(self.src_root, src_full_path)
594 src_lines = self._svn_readlines(self.src_root, src_full_path)
585
595
586 if change == 'delete':
596 if change == 'delete':
587 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
597 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
588 tgt_lines = []
598 tgt_lines = []
589 else:
599 else:
590 buf.write("+++ b/%s\t(revision %s)\n" % (
600 buf.write("+++ b/%s\t(revision %s)\n" % (
591 tgt_path, self.tgt_rev))
601 tgt_path, self.tgt_rev))
592 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
602 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
593
603
594 if not self.binary_content:
604 if not self.binary_content:
595 udiff = svn_diff.unified_diff(
605 udiff = svn_diff.unified_diff(
596 src_lines, tgt_lines, context=self.context,
606 src_lines, tgt_lines, context=self.context,
597 ignore_blank_lines=self.ignore_whitespace,
607 ignore_blank_lines=self.ignore_whitespace,
598 ignore_case=False,
608 ignore_case=False,
599 ignore_space_changes=self.ignore_whitespace)
609 ignore_space_changes=self.ignore_whitespace)
600 buf.writelines(udiff)
610 buf.writelines(udiff)
601
611
602 def _get_mime_type(self, path):
612 def _get_mime_type(self, path):
603 try:
613 try:
604 mime_type = svn.fs.node_prop(
614 mime_type = svn.fs.node_prop(
605 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
615 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
606 except svn.core.SubversionException:
616 except svn.core.SubversionException:
607 mime_type = svn.fs.node_prop(
617 mime_type = svn.fs.node_prop(
608 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
618 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
609 return mime_type
619 return mime_type
610
620
611 def _svn_readlines(self, fs_root, node_path):
621 def _svn_readlines(self, fs_root, node_path):
612 if self.binary_content:
622 if self.binary_content:
613 return []
623 return []
614 node_kind = svn.fs.check_path(fs_root, node_path)
624 node_kind = svn.fs.check_path(fs_root, node_path)
615 if node_kind not in (
625 if node_kind not in (
616 svn.core.svn_node_file, svn.core.svn_node_symlink):
626 svn.core.svn_node_file, svn.core.svn_node_symlink):
617 return []
627 return []
618 content = svn.core.Stream(
628 content = svn.core.Stream(
619 svn.fs.file_contents(fs_root, node_path)).read()
629 svn.fs.file_contents(fs_root, node_path)).read()
620 return content.splitlines(True)
630 return content.splitlines(True)
621
631
622
632
623
633
624 class DiffChangeEditor(svn.delta.Editor):
634 class DiffChangeEditor(svn.delta.Editor):
625 """
635 """
626 Records changes between two given revisions
636 Records changes between two given revisions
627 """
637 """
628
638
629 def __init__(self):
639 def __init__(self):
630 self.changes = []
640 self.changes = []
631
641
632 def delete_entry(self, path, revision, parent_baton, pool=None):
642 def delete_entry(self, path, revision, parent_baton, pool=None):
633 self.changes.append((path, None, 'delete'))
643 self.changes.append((path, None, 'delete'))
634
644
635 def add_file(
645 def add_file(
636 self, path, parent_baton, copyfrom_path, copyfrom_revision,
646 self, path, parent_baton, copyfrom_path, copyfrom_revision,
637 file_pool=None):
647 file_pool=None):
638 self.changes.append((path, 'file', 'add'))
648 self.changes.append((path, 'file', 'add'))
639
649
640 def open_file(self, path, parent_baton, base_revision, file_pool=None):
650 def open_file(self, path, parent_baton, base_revision, file_pool=None):
641 self.changes.append((path, 'file', 'change'))
651 self.changes.append((path, 'file', 'change'))
642
652
643
653
644 def authorization_callback_allow_all(root, path, pool):
654 def authorization_callback_allow_all(root, path, pool):
645 return True
655 return True
646
656
647
657
648 class TxnNodeProcessor(object):
658 class TxnNodeProcessor(object):
649 """
659 """
650 Utility to process the change of one node within a transaction root.
660 Utility to process the change of one node within a transaction root.
651
661
652 It encapsulates the knowledge of how to add, update or remove
662 It encapsulates the knowledge of how to add, update or remove
653 a node for a given transaction root. The purpose is to support the method
663 a node for a given transaction root. The purpose is to support the method
654 `SvnRemote.commit`.
664 `SvnRemote.commit`.
655 """
665 """
656
666
657 def __init__(self, node, txn_root):
667 def __init__(self, node, txn_root):
658 assert isinstance(node['path'], str)
668 assert isinstance(node['path'], str)
659
669
660 self.node = node
670 self.node = node
661 self.txn_root = txn_root
671 self.txn_root = txn_root
662
672
663 def update(self):
673 def update(self):
664 self._ensure_parent_dirs()
674 self._ensure_parent_dirs()
665 self._add_file_if_node_does_not_exist()
675 self._add_file_if_node_does_not_exist()
666 self._update_file_content()
676 self._update_file_content()
667 self._update_file_properties()
677 self._update_file_properties()
668
678
669 def remove(self):
679 def remove(self):
670 svn.fs.delete(self.txn_root, self.node['path'])
680 svn.fs.delete(self.txn_root, self.node['path'])
671 # TODO: Clean up directory if empty
681 # TODO: Clean up directory if empty
672
682
673 def _ensure_parent_dirs(self):
683 def _ensure_parent_dirs(self):
674 curdir = vcspath.dirname(self.node['path'])
684 curdir = vcspath.dirname(self.node['path'])
675 dirs_to_create = []
685 dirs_to_create = []
676 while not self._svn_path_exists(curdir):
686 while not self._svn_path_exists(curdir):
677 dirs_to_create.append(curdir)
687 dirs_to_create.append(curdir)
678 curdir = vcspath.dirname(curdir)
688 curdir = vcspath.dirname(curdir)
679
689
680 for curdir in reversed(dirs_to_create):
690 for curdir in reversed(dirs_to_create):
681 log.debug('Creating missing directory "%s"', curdir)
691 log.debug('Creating missing directory "%s"', curdir)
682 svn.fs.make_dir(self.txn_root, curdir)
692 svn.fs.make_dir(self.txn_root, curdir)
683
693
684 def _svn_path_exists(self, path):
694 def _svn_path_exists(self, path):
685 path_status = svn.fs.check_path(self.txn_root, path)
695 path_status = svn.fs.check_path(self.txn_root, path)
686 return path_status != svn.core.svn_node_none
696 return path_status != svn.core.svn_node_none
687
697
688 def _add_file_if_node_does_not_exist(self):
698 def _add_file_if_node_does_not_exist(self):
689 kind = svn.fs.check_path(self.txn_root, self.node['path'])
699 kind = svn.fs.check_path(self.txn_root, self.node['path'])
690 if kind == svn.core.svn_node_none:
700 if kind == svn.core.svn_node_none:
691 svn.fs.make_file(self.txn_root, self.node['path'])
701 svn.fs.make_file(self.txn_root, self.node['path'])
692
702
693 def _update_file_content(self):
703 def _update_file_content(self):
694 assert isinstance(self.node['content'], str)
704 assert isinstance(self.node['content'], str)
695 handler, baton = svn.fs.apply_textdelta(
705 handler, baton = svn.fs.apply_textdelta(
696 self.txn_root, self.node['path'], None, None)
706 self.txn_root, self.node['path'], None, None)
697 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
707 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
698
708
699 def _update_file_properties(self):
709 def _update_file_properties(self):
700 properties = self.node.get('properties', {})
710 properties = self.node.get('properties', {})
701 for key, value in properties.iteritems():
711 for key, value in properties.iteritems():
702 svn.fs.change_node_prop(
712 svn.fs.change_node_prop(
703 self.txn_root, self.node['path'], key, value)
713 self.txn_root, self.node['path'], key, value)
704
714
705
715
706 def apr_time_t(timestamp):
716 def apr_time_t(timestamp):
707 """
717 """
708 Convert a Python timestamp into APR timestamp type apr_time_t
718 Convert a Python timestamp into APR timestamp type apr_time_t
709 """
719 """
710 return timestamp * 1E6
720 return timestamp * 1E6
711
721
712
722
713 def svn_opt_revision_value_t(num):
723 def svn_opt_revision_value_t(num):
714 """
724 """
715 Put `num` into a `svn_opt_revision_value_t` structure.
725 Put `num` into a `svn_opt_revision_value_t` structure.
716 """
726 """
717 value = svn.core.svn_opt_revision_value_t()
727 value = svn.core.svn_opt_revision_value_t()
718 value.number = num
728 value.number = num
719 revision = svn.core.svn_opt_revision_t()
729 revision = svn.core.svn_opt_revision_t()
720 revision.kind = svn.core.svn_opt_revision_number
730 revision.kind = svn.core.svn_opt_revision_number
721 revision.value = value
731 revision.value = value
722 return revision
732 return revision
@@ -1,16 +1,16 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
@@ -1,57 +1,57 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import socket
18 import socket
19
19
20 import pytest
20 import pytest
21
21
22
22
23 def pytest_addoption(parser):
23 def pytest_addoption(parser):
24 parser.addoption(
24 parser.addoption(
25 '--repeat', type=int, default=100,
25 '--repeat', type=int, default=100,
26 help="Number of repetitions in performance tests.")
26 help="Number of repetitions in performance tests.")
27
27
28
28
29 @pytest.fixture(scope='session')
29 @pytest.fixture(scope='session')
30 def repeat(request):
30 def repeat(request):
31 """
31 """
32 The number of repetitions is based on this fixture.
32 The number of repetitions is based on this fixture.
33
33
34 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
34 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
35 tests are not too slow in our default test suite.
35 tests are not too slow in our default test suite.
36 """
36 """
37 return request.config.getoption('--repeat')
37 return request.config.getoption('--repeat')
38
38
39
39
40 @pytest.fixture(scope='session')
40 @pytest.fixture(scope='session')
41 def vcsserver_port(request):
41 def vcsserver_port(request):
42 port = get_available_port()
42 port = get_available_port()
43 print('Using vcsserver port %s' % (port, ))
43 print('Using vcsserver port %s' % (port, ))
44 return port
44 return port
45
45
46
46
47 def get_available_port():
47 def get_available_port():
48 family = socket.AF_INET
48 family = socket.AF_INET
49 socktype = socket.SOCK_STREAM
49 socktype = socket.SOCK_STREAM
50 host = '127.0.0.1'
50 host = '127.0.0.1'
51
51
52 mysocket = socket.socket(family, socktype)
52 mysocket = socket.socket(family, socktype)
53 mysocket.bind((host, 0))
53 mysocket.bind((host, 0))
54 port = mysocket.getsockname()[1]
54 port = mysocket.getsockname()[1]
55 mysocket.close()
55 mysocket.close()
56 del mysocket
56 del mysocket
57 return port
57 return port
@@ -1,86 +1,86 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import shutil
19 import shutil
20 import tempfile
20 import tempfile
21
21
22 import configobj
22 import configobj
23
23
24
24
25 class ContextINI(object):
25 class ContextINI(object):
26 """
26 """
27 Allows to create a new test.ini file as a copy of existing one with edited
27 Allows to create a new test.ini file as a copy of existing one with edited
28 data. If existing file is not present, it creates a new one. Example usage::
28 data. If existing file is not present, it creates a new one. Example usage::
29
29
30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
31 print 'vcsserver --config=%s' % new_test_ini
31 print 'vcsserver --config=%s' % new_test_ini
32 """
32 """
33
33
34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
35 destroy=True):
35 destroy=True):
36 self.ini_file_path = ini_file_path
36 self.ini_file_path = ini_file_path
37 self.ini_params = ini_params
37 self.ini_params = ini_params
38 self.new_path = None
38 self.new_path = None
39 self.new_path_prefix = new_file_prefix or 'test'
39 self.new_path_prefix = new_file_prefix or 'test'
40 self.destroy = destroy
40 self.destroy = destroy
41
41
42 def __enter__(self):
42 def __enter__(self):
43 _, pref = tempfile.mkstemp()
43 _, pref = tempfile.mkstemp()
44 loc = tempfile.gettempdir()
44 loc = tempfile.gettempdir()
45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
46 pref, self.new_path_prefix, self.ini_file_path))
46 pref, self.new_path_prefix, self.ini_file_path))
47
47
48 # copy ini file and modify according to the params, if we re-use a file
48 # copy ini file and modify according to the params, if we re-use a file
49 if os.path.isfile(self.ini_file_path):
49 if os.path.isfile(self.ini_file_path):
50 shutil.copy(self.ini_file_path, self.new_path)
50 shutil.copy(self.ini_file_path, self.new_path)
51 else:
51 else:
52 # create new dump file for configObj to write to.
52 # create new dump file for configObj to write to.
53 with open(self.new_path, 'wb'):
53 with open(self.new_path, 'wb'):
54 pass
54 pass
55
55
56 config = configobj.ConfigObj(
56 config = configobj.ConfigObj(
57 self.new_path, file_error=True, write_empty_values=True)
57 self.new_path, file_error=True, write_empty_values=True)
58
58
59 for data in self.ini_params:
59 for data in self.ini_params:
60 section, ini_params = data.items()[0]
60 section, ini_params = data.items()[0]
61 key, val = ini_params.items()[0]
61 key, val = ini_params.items()[0]
62 if section not in config:
62 if section not in config:
63 config[section] = {}
63 config[section] = {}
64 config[section][key] = val
64 config[section][key] = val
65
65
66 config.write()
66 config.write()
67 return self.new_path
67 return self.new_path
68
68
69 def __exit__(self, exc_type, exc_val, exc_tb):
69 def __exit__(self, exc_type, exc_val, exc_tb):
70 if self.destroy:
70 if self.destroy:
71 os.remove(self.new_path)
71 os.remove(self.new_path)
72
72
73
73
74 def no_newline_id_generator(test_name):
74 def no_newline_id_generator(test_name):
75 """
75 """
76 Generates a test name without spaces or newlines characters. Used for
76 Generates a test name without spaces or newlines characters. Used for
77 nicer output of progress of test
77 nicer output of progress of test
78 """
78 """
79 org_name = test_name
79 org_name = test_name
80 test_name = str(test_name)\
80 test_name = str(test_name)\
81 .replace('\n', '_N') \
81 .replace('\n', '_N') \
82 .replace('\r', '_N') \
82 .replace('\r', '_N') \
83 .replace('\t', '_T') \
83 .replace('\t', '_T') \
84 .replace(' ', '_S')
84 .replace(' ', '_S')
85
85
86 return test_name or 'test-with-empty-name'
86 return test_name or 'test-with-empty-name'
@@ -1,165 +1,165 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import inspect
18 import inspect
19
19
20 import pytest
20 import pytest
21 import dulwich.errors
21 import dulwich.errors
22 from mock import Mock, patch
22 from mock import Mock, patch
23
23
24 from vcsserver import git
24 from vcsserver import git
25
25
26
26
27 SAMPLE_REFS = {
27 SAMPLE_REFS = {
28 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
28 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
29 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
29 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
30 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
30 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
31 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
31 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
32 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
32 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
33 }
33 }
34
34
35
35
36 @pytest.fixture
36 @pytest.fixture
37 def git_remote():
37 def git_remote():
38 """
38 """
39 A GitRemote instance with a mock factory.
39 A GitRemote instance with a mock factory.
40 """
40 """
41 factory = Mock()
41 factory = Mock()
42 remote = git.GitRemote(factory)
42 remote = git.GitRemote(factory)
43 return remote
43 return remote
44
44
45
45
46 def test_discover_git_version(git_remote):
46 def test_discover_git_version(git_remote):
47 version = git_remote.discover_git_version()
47 version = git_remote.discover_git_version()
48 assert version
48 assert version
49
49
50
50
51 class TestGitFetch(object):
51 class TestGitFetch(object):
52 def setup(self):
52 def setup(self):
53 self.mock_repo = Mock()
53 self.mock_repo = Mock()
54 factory = Mock()
54 factory = Mock()
55 factory.repo = Mock(return_value=self.mock_repo)
55 factory.repo = Mock(return_value=self.mock_repo)
56 self.remote_git = git.GitRemote(factory)
56 self.remote_git = git.GitRemote(factory)
57
57
58 def test_fetches_all_when_no_commit_ids_specified(self):
58 def test_fetches_all_when_no_commit_ids_specified(self):
59 def side_effect(determine_wants, *args, **kwargs):
59 def side_effect(determine_wants, *args, **kwargs):
60 determine_wants(SAMPLE_REFS)
60 determine_wants(SAMPLE_REFS)
61
61
62 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
62 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
63 mock_fetch.side_effect = side_effect
63 mock_fetch.side_effect = side_effect
64 self.remote_git.pull(wire=None, url='/tmp/', apply_refs=False)
64 self.remote_git.pull(wire=None, url='/tmp/', apply_refs=False)
65 determine_wants = self.mock_repo.object_store.determine_wants_all
65 determine_wants = self.mock_repo.object_store.determine_wants_all
66 determine_wants.assert_called_once_with(SAMPLE_REFS)
66 determine_wants.assert_called_once_with(SAMPLE_REFS)
67
67
68 def test_fetches_specified_commits(self):
68 def test_fetches_specified_commits(self):
69 selected_refs = {
69 selected_refs = {
70 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
70 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
71 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
71 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
72 }
72 }
73
73
74 def side_effect(determine_wants, *args, **kwargs):
74 def side_effect(determine_wants, *args, **kwargs):
75 result = determine_wants(SAMPLE_REFS)
75 result = determine_wants(SAMPLE_REFS)
76 assert sorted(result) == sorted(selected_refs.values())
76 assert sorted(result) == sorted(selected_refs.values())
77 return result
77 return result
78
78
79 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
79 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
80 mock_fetch.side_effect = side_effect
80 mock_fetch.side_effect = side_effect
81 self.remote_git.pull(
81 self.remote_git.pull(
82 wire=None, url='/tmp/', apply_refs=False,
82 wire=None, url='/tmp/', apply_refs=False,
83 refs=selected_refs.keys())
83 refs=selected_refs.keys())
84 determine_wants = self.mock_repo.object_store.determine_wants_all
84 determine_wants = self.mock_repo.object_store.determine_wants_all
85 assert determine_wants.call_count == 0
85 assert determine_wants.call_count == 0
86
86
87 def test_get_remote_refs(self):
87 def test_get_remote_refs(self):
88 factory = Mock()
88 factory = Mock()
89 remote_git = git.GitRemote(factory)
89 remote_git = git.GitRemote(factory)
90 url = 'http://example.com/test/test.git'
90 url = 'http://example.com/test/test.git'
91 sample_refs = {
91 sample_refs = {
92 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
92 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
93 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
93 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
94 }
94 }
95
95
96 with patch('vcsserver.git.Repo', create=False) as mock_repo:
96 with patch('vcsserver.git.Repo', create=False) as mock_repo:
97 mock_repo().get_refs.return_value = sample_refs
97 mock_repo().get_refs.return_value = sample_refs
98 remote_refs = remote_git.get_remote_refs(wire=None, url=url)
98 remote_refs = remote_git.get_remote_refs(wire=None, url=url)
99 mock_repo().get_refs.assert_called_once_with()
99 mock_repo().get_refs.assert_called_once_with()
100 assert remote_refs == sample_refs
100 assert remote_refs == sample_refs
101
101
102 def test_remove_ref(self):
102 def test_remove_ref(self):
103 ref_to_remove = 'refs/tags/v0.1.9'
103 ref_to_remove = 'refs/tags/v0.1.9'
104 self.mock_repo.refs = SAMPLE_REFS.copy()
104 self.mock_repo.refs = SAMPLE_REFS.copy()
105 self.remote_git.remove_ref(None, ref_to_remove)
105 self.remote_git.remove_ref(None, ref_to_remove)
106 assert ref_to_remove not in self.mock_repo.refs
106 assert ref_to_remove not in self.mock_repo.refs
107
107
108
108
109 class TestReraiseSafeExceptions(object):
109 class TestReraiseSafeExceptions(object):
110 def test_method_decorated_with_reraise_safe_exceptions(self):
110 def test_method_decorated_with_reraise_safe_exceptions(self):
111 factory = Mock()
111 factory = Mock()
112 git_remote = git.GitRemote(factory)
112 git_remote = git.GitRemote(factory)
113
113
114 def fake_function():
114 def fake_function():
115 return None
115 return None
116
116
117 decorator = git.reraise_safe_exceptions(fake_function)
117 decorator = git.reraise_safe_exceptions(fake_function)
118
118
119 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
119 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
120 for method_name, method in methods:
120 for method_name, method in methods:
121 if not method_name.startswith('_'):
121 if not method_name.startswith('_'):
122 assert method.im_func.__code__ == decorator.__code__
122 assert method.im_func.__code__ == decorator.__code__
123
123
124 @pytest.mark.parametrize('side_effect, expected_type', [
124 @pytest.mark.parametrize('side_effect, expected_type', [
125 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
125 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
126 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
126 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
127 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
127 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
128 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
128 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
129 (dulwich.errors.HangupException(), 'error'),
129 (dulwich.errors.HangupException(), 'error'),
130 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
130 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
131 ])
131 ])
132 def test_safe_exceptions_reraised(self, side_effect, expected_type):
132 def test_safe_exceptions_reraised(self, side_effect, expected_type):
133 @git.reraise_safe_exceptions
133 @git.reraise_safe_exceptions
134 def fake_method():
134 def fake_method():
135 raise side_effect
135 raise side_effect
136
136
137 with pytest.raises(Exception) as exc_info:
137 with pytest.raises(Exception) as exc_info:
138 fake_method()
138 fake_method()
139 assert type(exc_info.value) == Exception
139 assert type(exc_info.value) == Exception
140 assert exc_info.value._vcs_kind == expected_type
140 assert exc_info.value._vcs_kind == expected_type
141
141
142
142
143 class TestDulwichRepoWrapper(object):
143 class TestDulwichRepoWrapper(object):
144 def test_calls_close_on_delete(self):
144 def test_calls_close_on_delete(self):
145 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
145 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
146 with isdir_patcher:
146 with isdir_patcher:
147 repo = git.Repo('/tmp/abcde')
147 repo = git.Repo('/tmp/abcde')
148 with patch.object(git.DulwichRepo, 'close') as close_mock:
148 with patch.object(git.DulwichRepo, 'close') as close_mock:
149 del repo
149 del repo
150 close_mock.assert_called_once_with()
150 close_mock.assert_called_once_with()
151
151
152
152
153 class TestGitFactory(object):
153 class TestGitFactory(object):
154 def test_create_repo_returns_dulwich_wrapper(self):
154 def test_create_repo_returns_dulwich_wrapper(self):
155
155
156 with patch('vcsserver.lib.rc_cache.region_meta.dogpile_cache_regions') as mock:
156 with patch('vcsserver.lib.rc_cache.region_meta.dogpile_cache_regions') as mock:
157 mock.side_effect = {'repo_objects': ''}
157 mock.side_effect = {'repo_objects': ''}
158 factory = git.GitFactory()
158 factory = git.GitFactory()
159 wire = {
159 wire = {
160 'path': '/tmp/abcde'
160 'path': '/tmp/abcde'
161 }
161 }
162 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
162 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
163 with isdir_patcher:
163 with isdir_patcher:
164 result = factory._create_repo(wire, True)
164 result = factory._create_repo(wire, True)
165 assert isinstance(result, git.Repo)
165 assert isinstance(result, git.Repo)
@@ -1,127 +1,127 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import inspect
18 import inspect
19 import sys
19 import sys
20 import traceback
20 import traceback
21
21
22 import pytest
22 import pytest
23 from mercurial.error import LookupError
23 from mercurial.error import LookupError
24 from mock import Mock, MagicMock, patch
24 from mock import Mock, MagicMock, patch
25
25
26 from vcsserver import exceptions, hg, hgcompat
26 from vcsserver import exceptions, hg, hgcompat
27
27
28
28
29 class TestHGLookup(object):
29 class TestHGLookup(object):
30 def setup(self):
30 def setup(self):
31 self.mock_repo = MagicMock()
31 self.mock_repo = MagicMock()
32 self.mock_repo.__getitem__.side_effect = LookupError(
32 self.mock_repo.__getitem__.side_effect = LookupError(
33 'revision_or_commit_id', 'index', 'message')
33 'revision_or_commit_id', 'index', 'message')
34 factory = Mock()
34 factory = Mock()
35 factory.repo = Mock(return_value=self.mock_repo)
35 factory.repo = Mock(return_value=self.mock_repo)
36 self.remote_hg = hg.HgRemote(factory)
36 self.remote_hg = hg.HgRemote(factory)
37
37
38 def test_fail_lookup_hg(self):
38 def test_fail_lookup_hg(self):
39 with pytest.raises(Exception) as exc_info:
39 with pytest.raises(Exception) as exc_info:
40 self.remote_hg.lookup(
40 self.remote_hg.lookup(
41 wire=None, revision='revision_or_commit_id', both=True)
41 wire=None, revision='revision_or_commit_id', both=True)
42
42
43 assert exc_info.value._vcs_kind == 'lookup'
43 assert exc_info.value._vcs_kind == 'lookup'
44 assert 'revision_or_commit_id' in exc_info.value.args
44 assert 'revision_or_commit_id' in exc_info.value.args
45
45
46
46
47 class TestDiff(object):
47 class TestDiff(object):
48 def test_raising_safe_exception_when_lookup_failed(self):
48 def test_raising_safe_exception_when_lookup_failed(self):
49 repo = Mock()
49 repo = Mock()
50 factory = Mock()
50 factory = Mock()
51 factory.repo = Mock(return_value=repo)
51 factory.repo = Mock(return_value=repo)
52 hg_remote = hg.HgRemote(factory)
52 hg_remote = hg.HgRemote(factory)
53 with patch('mercurial.patch.diff') as diff_mock:
53 with patch('mercurial.patch.diff') as diff_mock:
54 diff_mock.side_effect = LookupError(
54 diff_mock.side_effect = LookupError(
55 'deadbeef', 'index', 'message')
55 'deadbeef', 'index', 'message')
56 with pytest.raises(Exception) as exc_info:
56 with pytest.raises(Exception) as exc_info:
57 hg_remote.diff(
57 hg_remote.diff(
58 wire=None, rev1='deadbeef', rev2='deadbee1',
58 wire=None, rev1='deadbeef', rev2='deadbee1',
59 file_filter=None, opt_git=True, opt_ignorews=True,
59 file_filter=None, opt_git=True, opt_ignorews=True,
60 context=3)
60 context=3)
61 assert type(exc_info.value) == Exception
61 assert type(exc_info.value) == Exception
62 assert exc_info.value._vcs_kind == 'lookup'
62 assert exc_info.value._vcs_kind == 'lookup'
63
63
64
64
65 class TestReraiseSafeExceptions(object):
65 class TestReraiseSafeExceptions(object):
66 def test_method_decorated_with_reraise_safe_exceptions(self):
66 def test_method_decorated_with_reraise_safe_exceptions(self):
67 factory = Mock()
67 factory = Mock()
68 hg_remote = hg.HgRemote(factory)
68 hg_remote = hg.HgRemote(factory)
69 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
69 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
70 decorator = hg.reraise_safe_exceptions(None)
70 decorator = hg.reraise_safe_exceptions(None)
71 for method_name, method in methods:
71 for method_name, method in methods:
72 if not method_name.startswith('_'):
72 if not method_name.startswith('_'):
73 assert method.im_func.__code__ == decorator.__code__
73 assert method.im_func.__code__ == decorator.__code__
74
74
75 @pytest.mark.parametrize('side_effect, expected_type', [
75 @pytest.mark.parametrize('side_effect, expected_type', [
76 (hgcompat.Abort(), 'abort'),
76 (hgcompat.Abort(), 'abort'),
77 (hgcompat.InterventionRequired(), 'abort'),
77 (hgcompat.InterventionRequired(), 'abort'),
78 (hgcompat.RepoLookupError(), 'lookup'),
78 (hgcompat.RepoLookupError(), 'lookup'),
79 (hgcompat.LookupError('deadbeef', 'index', 'message'), 'lookup'),
79 (hgcompat.LookupError('deadbeef', 'index', 'message'), 'lookup'),
80 (hgcompat.RepoError(), 'error'),
80 (hgcompat.RepoError(), 'error'),
81 (hgcompat.RequirementError(), 'requirement'),
81 (hgcompat.RequirementError(), 'requirement'),
82 ])
82 ])
83 def test_safe_exceptions_reraised(self, side_effect, expected_type):
83 def test_safe_exceptions_reraised(self, side_effect, expected_type):
84 @hg.reraise_safe_exceptions
84 @hg.reraise_safe_exceptions
85 def fake_method():
85 def fake_method():
86 raise side_effect
86 raise side_effect
87
87
88 with pytest.raises(Exception) as exc_info:
88 with pytest.raises(Exception) as exc_info:
89 fake_method()
89 fake_method()
90 assert type(exc_info.value) == Exception
90 assert type(exc_info.value) == Exception
91 assert exc_info.value._vcs_kind == expected_type
91 assert exc_info.value._vcs_kind == expected_type
92
92
93 def test_keeps_original_traceback(self):
93 def test_keeps_original_traceback(self):
94 @hg.reraise_safe_exceptions
94 @hg.reraise_safe_exceptions
95 def fake_method():
95 def fake_method():
96 try:
96 try:
97 raise hgcompat.Abort()
97 raise hgcompat.Abort()
98 except:
98 except:
99 self.original_traceback = traceback.format_tb(
99 self.original_traceback = traceback.format_tb(
100 sys.exc_info()[2])
100 sys.exc_info()[2])
101 raise
101 raise
102
102
103 try:
103 try:
104 fake_method()
104 fake_method()
105 except Exception:
105 except Exception:
106 new_traceback = traceback.format_tb(sys.exc_info()[2])
106 new_traceback = traceback.format_tb(sys.exc_info()[2])
107
107
108 new_traceback_tail = new_traceback[-len(self.original_traceback):]
108 new_traceback_tail = new_traceback[-len(self.original_traceback):]
109 assert new_traceback_tail == self.original_traceback
109 assert new_traceback_tail == self.original_traceback
110
110
111 def test_maps_unknow_exceptions_to_unhandled(self):
111 def test_maps_unknow_exceptions_to_unhandled(self):
112 @hg.reraise_safe_exceptions
112 @hg.reraise_safe_exceptions
113 def stub_method():
113 def stub_method():
114 raise ValueError('stub')
114 raise ValueError('stub')
115
115
116 with pytest.raises(Exception) as exc_info:
116 with pytest.raises(Exception) as exc_info:
117 stub_method()
117 stub_method()
118 assert exc_info.value._vcs_kind == 'unhandled'
118 assert exc_info.value._vcs_kind == 'unhandled'
119
119
120 def test_does_not_map_known_exceptions(self):
120 def test_does_not_map_known_exceptions(self):
121 @hg.reraise_safe_exceptions
121 @hg.reraise_safe_exceptions
122 def stub_method():
122 def stub_method():
123 raise exceptions.LookupException()('stub')
123 raise exceptions.LookupException()('stub')
124
124
125 with pytest.raises(Exception) as exc_info:
125 with pytest.raises(Exception) as exc_info:
126 stub_method()
126 stub_method()
127 assert exc_info.value._vcs_kind == 'lookup'
127 assert exc_info.value._vcs_kind == 'lookup'
@@ -1,124 +1,124 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import mock
18 import mock
19 import pytest
19 import pytest
20
20
21 from vcsserver import hgcompat, hgpatches
21 from vcsserver import hgcompat, hgpatches
22
22
23
23
24 LARGEFILES_CAPABILITY = 'largefiles=serve'
24 LARGEFILES_CAPABILITY = 'largefiles=serve'
25
25
26
26
27 def test_patch_largefiles_capabilities_applies_patch(
27 def test_patch_largefiles_capabilities_applies_patch(
28 patched_capabilities):
28 patched_capabilities):
29 lfproto = hgcompat.largefiles.proto
29 lfproto = hgcompat.largefiles.proto
30 hgpatches.patch_largefiles_capabilities()
30 hgpatches.patch_largefiles_capabilities()
31 assert lfproto._capabilities.func_name == '_dynamic_capabilities'
31 assert lfproto._capabilities.func_name == '_dynamic_capabilities'
32
32
33
33
34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
36 orig_capabilities):
36 orig_capabilities):
37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
38 hgcompat.largefiles.proto, stub_extensions)
38 hgcompat.largefiles.proto, stub_extensions)
39
39
40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
41
41
42 stub_extensions.assert_called_once_with(stub_ui)
42 stub_extensions.assert_called_once_with(stub_ui)
43 assert LARGEFILES_CAPABILITY not in caps
43 assert LARGEFILES_CAPABILITY not in caps
44
44
45
45
46 def test_dynamic_capabilities_ignores_updated_capabilities(
46 def test_dynamic_capabilities_ignores_updated_capabilities(
47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
48 orig_capabilities):
48 orig_capabilities):
49 stub_extensions.return_value = [('largefiles', mock.Mock())]
49 stub_extensions.return_value = [('largefiles', mock.Mock())]
50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
51 hgcompat.largefiles.proto, stub_extensions)
51 hgcompat.largefiles.proto, stub_extensions)
52
52
53 # This happens when the extension is loaded for the first time, important
53 # This happens when the extension is loaded for the first time, important
54 # to ensure that an updated function is correctly picked up.
54 # to ensure that an updated function is correctly picked up.
55 hgcompat.largefiles.proto._capabilities = mock.Mock(
55 hgcompat.largefiles.proto._capabilities = mock.Mock(
56 side_effect=Exception('Must not be called'))
56 side_effect=Exception('Must not be called'))
57
57
58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
59
59
60
60
61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
63 orig_capabilities):
63 orig_capabilities):
64 stub_extensions.return_value = [('largefiles', mock.Mock())]
64 stub_extensions.return_value = [('largefiles', mock.Mock())]
65
65
66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
67 hgcompat.largefiles.proto, stub_extensions)
67 hgcompat.largefiles.proto, stub_extensions)
68
68
69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
70
70
71 stub_extensions.assert_called_once_with(stub_ui)
71 stub_extensions.assert_called_once_with(stub_ui)
72 assert LARGEFILES_CAPABILITY in caps
72 assert LARGEFILES_CAPABILITY in caps
73
73
74
74
75 def test_hgsubversion_import():
75 def test_hgsubversion_import():
76 from hgsubversion import svnrepo
76 from hgsubversion import svnrepo
77 assert svnrepo
77 assert svnrepo
78
78
79
79
80 @pytest.fixture
80 @pytest.fixture
81 def patched_capabilities(request):
81 def patched_capabilities(request):
82 """
82 """
83 Patch in `capabilitiesorig` and restore both capability functions.
83 Patch in `capabilitiesorig` and restore both capability functions.
84 """
84 """
85 lfproto = hgcompat.largefiles.proto
85 lfproto = hgcompat.largefiles.proto
86 orig_capabilities = lfproto._capabilities
86 orig_capabilities = lfproto._capabilities
87
87
88 @request.addfinalizer
88 @request.addfinalizer
89 def restore():
89 def restore():
90 lfproto._capabilities = orig_capabilities
90 lfproto._capabilities = orig_capabilities
91
91
92
92
93 @pytest.fixture
93 @pytest.fixture
94 def stub_repo(stub_ui):
94 def stub_repo(stub_ui):
95 repo = mock.Mock()
95 repo = mock.Mock()
96 repo.ui = stub_ui
96 repo.ui = stub_ui
97 return repo
97 return repo
98
98
99
99
100 @pytest.fixture
100 @pytest.fixture
101 def stub_proto(stub_ui):
101 def stub_proto(stub_ui):
102 proto = mock.Mock()
102 proto = mock.Mock()
103 proto.ui = stub_ui
103 proto.ui = stub_ui
104 return proto
104 return proto
105
105
106
106
107 @pytest.fixture
107 @pytest.fixture
108 def orig_capabilities():
108 def orig_capabilities():
109 from mercurial.wireprotov1server import wireprotocaps
109 from mercurial.wireprotov1server import wireprotocaps
110
110
111 def _capabilities(repo, proto):
111 def _capabilities(repo, proto):
112 return wireprotocaps
112 return wireprotocaps
113 return _capabilities
113 return _capabilities
114
114
115
115
116 @pytest.fixture
116 @pytest.fixture
117 def stub_ui():
117 def stub_ui():
118 return hgcompat.ui.ui()
118 return hgcompat.ui.ui()
119
119
120
120
121 @pytest.fixture
121 @pytest.fixture
122 def stub_extensions():
122 def stub_extensions():
123 extensions = mock.Mock(return_value=tuple())
123 extensions = mock.Mock(return_value=tuple())
124 return extensions
124 return extensions
@@ -1,241 +1,241 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import contextlib
18 import contextlib
19 import io
19 import io
20 import threading
20 import threading
21 from BaseHTTPServer import BaseHTTPRequestHandler
21 from BaseHTTPServer import BaseHTTPRequestHandler
22 from SocketServer import TCPServer
22 from SocketServer import TCPServer
23
23
24 import mercurial.ui
24 import mercurial.ui
25 import mock
25 import mock
26 import pytest
26 import pytest
27 import simplejson as json
27 import simplejson as json
28
28
29 from vcsserver import hooks
29 from vcsserver import hooks
30
30
31
31
32 def get_hg_ui(extras=None):
32 def get_hg_ui(extras=None):
33 """Create a Config object with a valid RC_SCM_DATA entry."""
33 """Create a Config object with a valid RC_SCM_DATA entry."""
34 extras = extras or {}
34 extras = extras or {}
35 required_extras = {
35 required_extras = {
36 'username': '',
36 'username': '',
37 'repository': '',
37 'repository': '',
38 'locked_by': '',
38 'locked_by': '',
39 'scm': '',
39 'scm': '',
40 'make_lock': '',
40 'make_lock': '',
41 'action': '',
41 'action': '',
42 'ip': '',
42 'ip': '',
43 'hooks_uri': 'fake_hooks_uri',
43 'hooks_uri': 'fake_hooks_uri',
44 }
44 }
45 required_extras.update(extras)
45 required_extras.update(extras)
46 hg_ui = mercurial.ui.ui()
46 hg_ui = mercurial.ui.ui()
47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
48
48
49 return hg_ui
49 return hg_ui
50
50
51
51
52 def test_git_pre_receive_is_disabled():
52 def test_git_pre_receive_is_disabled():
53 extras = {'hooks': ['pull']}
53 extras = {'hooks': ['pull']}
54 response = hooks.git_pre_receive(None, None,
54 response = hooks.git_pre_receive(None, None,
55 {'RC_SCM_DATA': json.dumps(extras)})
55 {'RC_SCM_DATA': json.dumps(extras)})
56
56
57 assert response == 0
57 assert response == 0
58
58
59
59
60 def test_git_post_receive_is_disabled():
60 def test_git_post_receive_is_disabled():
61 extras = {'hooks': ['pull']}
61 extras = {'hooks': ['pull']}
62 response = hooks.git_post_receive(None, '',
62 response = hooks.git_post_receive(None, '',
63 {'RC_SCM_DATA': json.dumps(extras)})
63 {'RC_SCM_DATA': json.dumps(extras)})
64
64
65 assert response == 0
65 assert response == 0
66
66
67
67
68 def test_git_post_receive_calls_repo_size():
68 def test_git_post_receive_calls_repo_size():
69 extras = {'hooks': ['push', 'repo_size']}
69 extras = {'hooks': ['push', 'repo_size']}
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 hooks.git_post_receive(
71 hooks.git_post_receive(
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
73 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
75 expected_calls = [
75 expected_calls = [
76 mock.call('repo_size', extras, mock.ANY),
76 mock.call('repo_size', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
78 ]
78 ]
79 assert call_hook_mock.call_args_list == expected_calls
79 assert call_hook_mock.call_args_list == expected_calls
80
80
81
81
82 def test_git_post_receive_does_not_call_disabled_repo_size():
82 def test_git_post_receive_does_not_call_disabled_repo_size():
83 extras = {'hooks': ['push']}
83 extras = {'hooks': ['push']}
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
85 hooks.git_post_receive(
85 hooks.git_post_receive(
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
87 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
87 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
89 expected_calls = [
89 expected_calls = [
90 mock.call('post_push', extras, mock.ANY)
90 mock.call('post_push', extras, mock.ANY)
91 ]
91 ]
92 assert call_hook_mock.call_args_list == expected_calls
92 assert call_hook_mock.call_args_list == expected_calls
93
93
94
94
95 def test_repo_size_exception_does_not_affect_git_post_receive():
95 def test_repo_size_exception_does_not_affect_git_post_receive():
96 extras = {'hooks': ['push', 'repo_size']}
96 extras = {'hooks': ['push', 'repo_size']}
97 status = 0
97 status = 0
98
98
99 def side_effect(name, *args, **kwargs):
99 def side_effect(name, *args, **kwargs):
100 if name == 'repo_size':
100 if name == 'repo_size':
101 raise Exception('Fake exception')
101 raise Exception('Fake exception')
102 else:
102 else:
103 return status
103 return status
104
104
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
106 call_hook_mock.side_effect = side_effect
106 call_hook_mock.side_effect = side_effect
107 result = hooks.git_post_receive(
107 result = hooks.git_post_receive(
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
109 assert result == status
109 assert result == status
110
110
111
111
112 def test_git_pre_pull_is_disabled():
112 def test_git_pre_pull_is_disabled():
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
114
114
115
115
116 def test_git_post_pull_is_disabled():
116 def test_git_post_pull_is_disabled():
117 assert (
117 assert (
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
119
119
120
120
121 class TestGetHooksClient(object):
121 class TestGetHooksClient(object):
122
122
123 def test_returns_http_client_when_protocol_matches(self):
123 def test_returns_http_client_when_protocol_matches(self):
124 hooks_uri = 'localhost:8000'
124 hooks_uri = 'localhost:8000'
125 result = hooks._get_hooks_client({
125 result = hooks._get_hooks_client({
126 'hooks_uri': hooks_uri,
126 'hooks_uri': hooks_uri,
127 'hooks_protocol': 'http'
127 'hooks_protocol': 'http'
128 })
128 })
129 assert isinstance(result, hooks.HooksHttpClient)
129 assert isinstance(result, hooks.HooksHttpClient)
130 assert result.hooks_uri == hooks_uri
130 assert result.hooks_uri == hooks_uri
131
131
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
133 fake_module = mock.Mock()
133 fake_module = mock.Mock()
134 import_patcher = mock.patch.object(
134 import_patcher = mock.patch.object(
135 hooks.importlib, 'import_module', return_value=fake_module)
135 hooks.importlib, 'import_module', return_value=fake_module)
136 fake_module_name = 'fake.module'
136 fake_module_name = 'fake.module'
137 with import_patcher as import_mock:
137 with import_patcher as import_mock:
138 result = hooks._get_hooks_client(
138 result = hooks._get_hooks_client(
139 {'hooks_module': fake_module_name})
139 {'hooks_module': fake_module_name})
140
140
141 import_mock.assert_called_once_with(fake_module_name)
141 import_mock.assert_called_once_with(fake_module_name)
142 assert isinstance(result, hooks.HooksDummyClient)
142 assert isinstance(result, hooks.HooksDummyClient)
143 assert result._hooks_module == fake_module
143 assert result._hooks_module == fake_module
144
144
145
145
146 class TestHooksHttpClient(object):
146 class TestHooksHttpClient(object):
147 def test_init_sets_hooks_uri(self):
147 def test_init_sets_hooks_uri(self):
148 uri = 'localhost:3000'
148 uri = 'localhost:3000'
149 client = hooks.HooksHttpClient(uri)
149 client = hooks.HooksHttpClient(uri)
150 assert client.hooks_uri == uri
150 assert client.hooks_uri == uri
151
151
152 def test_serialize_returns_json_string(self):
152 def test_serialize_returns_json_string(self):
153 client = hooks.HooksHttpClient('localhost:3000')
153 client = hooks.HooksHttpClient('localhost:3000')
154 hook_name = 'test'
154 hook_name = 'test'
155 extras = {
155 extras = {
156 'first': 1,
156 'first': 1,
157 'second': 'two'
157 'second': 'two'
158 }
158 }
159 result = client._serialize(hook_name, extras)
159 result = client._serialize(hook_name, extras)
160 expected_result = json.dumps({
160 expected_result = json.dumps({
161 'method': hook_name,
161 'method': hook_name,
162 'extras': extras
162 'extras': extras
163 })
163 })
164 assert result == expected_result
164 assert result == expected_result
165
165
166 def test_call_queries_http_server(self, http_mirror):
166 def test_call_queries_http_server(self, http_mirror):
167 client = hooks.HooksHttpClient(http_mirror.uri)
167 client = hooks.HooksHttpClient(http_mirror.uri)
168 hook_name = 'test'
168 hook_name = 'test'
169 extras = {
169 extras = {
170 'first': 1,
170 'first': 1,
171 'second': 'two'
171 'second': 'two'
172 }
172 }
173 result = client(hook_name, extras)
173 result = client(hook_name, extras)
174 expected_result = {
174 expected_result = {
175 'method': hook_name,
175 'method': hook_name,
176 'extras': extras
176 'extras': extras
177 }
177 }
178 assert result == expected_result
178 assert result == expected_result
179
179
180
180
181 class TestHooksDummyClient(object):
181 class TestHooksDummyClient(object):
182 def test_init_imports_hooks_module(self):
182 def test_init_imports_hooks_module(self):
183 hooks_module_name = 'rhodecode.fake.module'
183 hooks_module_name = 'rhodecode.fake.module'
184 hooks_module = mock.MagicMock()
184 hooks_module = mock.MagicMock()
185
185
186 import_patcher = mock.patch.object(
186 import_patcher = mock.patch.object(
187 hooks.importlib, 'import_module', return_value=hooks_module)
187 hooks.importlib, 'import_module', return_value=hooks_module)
188 with import_patcher as import_mock:
188 with import_patcher as import_mock:
189 client = hooks.HooksDummyClient(hooks_module_name)
189 client = hooks.HooksDummyClient(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
191 assert client._hooks_module == hooks_module
191 assert client._hooks_module == hooks_module
192
192
193 def test_call_returns_hook_result(self):
193 def test_call_returns_hook_result(self):
194 hooks_module_name = 'rhodecode.fake.module'
194 hooks_module_name = 'rhodecode.fake.module'
195 hooks_module = mock.MagicMock()
195 hooks_module = mock.MagicMock()
196 import_patcher = mock.patch.object(
196 import_patcher = mock.patch.object(
197 hooks.importlib, 'import_module', return_value=hooks_module)
197 hooks.importlib, 'import_module', return_value=hooks_module)
198 with import_patcher:
198 with import_patcher:
199 client = hooks.HooksDummyClient(hooks_module_name)
199 client = hooks.HooksDummyClient(hooks_module_name)
200
200
201 result = client('post_push', {})
201 result = client('post_push', {})
202 hooks_module.Hooks.assert_called_once_with()
202 hooks_module.Hooks.assert_called_once_with()
203 assert result == hooks_module.Hooks().__enter__().post_push()
203 assert result == hooks_module.Hooks().__enter__().post_push()
204
204
205
205
206 @pytest.fixture
206 @pytest.fixture
207 def http_mirror(request):
207 def http_mirror(request):
208 server = MirrorHttpServer()
208 server = MirrorHttpServer()
209 request.addfinalizer(server.stop)
209 request.addfinalizer(server.stop)
210 return server
210 return server
211
211
212
212
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
214 def do_POST(self):
214 def do_POST(self):
215 length = int(self.headers['Content-Length'])
215 length = int(self.headers['Content-Length'])
216 body = self.rfile.read(length).decode('utf-8')
216 body = self.rfile.read(length).decode('utf-8')
217 self.send_response(200)
217 self.send_response(200)
218 self.end_headers()
218 self.end_headers()
219 self.wfile.write(body)
219 self.wfile.write(body)
220
220
221
221
222 class MirrorHttpServer(object):
222 class MirrorHttpServer(object):
223 ip_address = '127.0.0.1'
223 ip_address = '127.0.0.1'
224 port = 0
224 port = 0
225
225
226 def __init__(self):
226 def __init__(self):
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
228 _, self.port = self._daemon.server_address
228 _, self.port = self._daemon.server_address
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
230 self._thread.daemon = True
230 self._thread.daemon = True
231 self._thread.start()
231 self._thread.start()
232
232
233 def stop(self):
233 def stop(self):
234 self._daemon.shutdown()
234 self._daemon.shutdown()
235 self._thread.join()
235 self._thread.join()
236 self._daemon = None
236 self._daemon = None
237 self._thread = None
237 self._thread = None
238
238
239 @property
239 @property
240 def uri(self):
240 def uri(self):
241 return '{}:{}'.format(self.ip_address, self.port)
241 return '{}:{}'.format(self.ip_address, self.port)
@@ -1,206 +1,206 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19 import sys
19 import sys
20 import stat
20 import stat
21 import pytest
21 import pytest
22 import vcsserver
22 import vcsserver
23 import tempfile
23 import tempfile
24 from vcsserver import hook_utils
24 from vcsserver import hook_utils
25 from vcsserver.tests.fixture import no_newline_id_generator
25 from vcsserver.tests.fixture import no_newline_id_generator
26 from vcsserver.utils import AttributeDict
26 from vcsserver.utils import AttributeDict
27
27
28
28
29 class TestCheckRhodecodeHook(object):
29 class TestCheckRhodecodeHook(object):
30
30
31 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
31 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
32 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
32 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
33 with open(hook, 'wb') as f:
33 with open(hook, 'wb') as f:
34 f.write('dummy test')
34 f.write('dummy test')
35 result = hook_utils.check_rhodecode_hook(hook)
35 result = hook_utils.check_rhodecode_hook(hook)
36 assert result is False
36 assert result is False
37
37
38 def test_returns_true_when_no_hook_file_found(self, tmpdir):
38 def test_returns_true_when_no_hook_file_found(self, tmpdir):
39 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
39 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
40 result = hook_utils.check_rhodecode_hook(hook)
40 result = hook_utils.check_rhodecode_hook(hook)
41 assert result
41 assert result
42
42
43 @pytest.mark.parametrize("file_content, expected_result", [
43 @pytest.mark.parametrize("file_content, expected_result", [
44 ("RC_HOOK_VER = '3.3.3'\n", True),
44 ("RC_HOOK_VER = '3.3.3'\n", True),
45 ("RC_HOOK = '3.3.3'\n", False),
45 ("RC_HOOK = '3.3.3'\n", False),
46 ], ids=no_newline_id_generator)
46 ], ids=no_newline_id_generator)
47 def test_signatures(self, file_content, expected_result, tmpdir):
47 def test_signatures(self, file_content, expected_result, tmpdir):
48 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
48 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
49 with open(hook, 'wb') as f:
49 with open(hook, 'wb') as f:
50 f.write(file_content)
50 f.write(file_content)
51
51
52 result = hook_utils.check_rhodecode_hook(hook)
52 result = hook_utils.check_rhodecode_hook(hook)
53
53
54 assert result is expected_result
54 assert result is expected_result
55
55
56
56
57 class BaseInstallHooks(object):
57 class BaseInstallHooks(object):
58 HOOK_FILES = ()
58 HOOK_FILES = ()
59
59
60 def _check_hook_file_mode(self, file_path):
60 def _check_hook_file_mode(self, file_path):
61 assert os.path.exists(file_path), 'path %s missing' % file_path
61 assert os.path.exists(file_path), 'path %s missing' % file_path
62 stat_info = os.stat(file_path)
62 stat_info = os.stat(file_path)
63
63
64 file_mode = stat.S_IMODE(stat_info.st_mode)
64 file_mode = stat.S_IMODE(stat_info.st_mode)
65 expected_mode = int('755', 8)
65 expected_mode = int('755', 8)
66 assert expected_mode == file_mode
66 assert expected_mode == file_mode
67
67
68 def _check_hook_file_content(self, file_path, executable):
68 def _check_hook_file_content(self, file_path, executable):
69 executable = executable or sys.executable
69 executable = executable or sys.executable
70 with open(file_path, 'rt') as hook_file:
70 with open(file_path, 'rt') as hook_file:
71 content = hook_file.read()
71 content = hook_file.read()
72
72
73 expected_env = '#!{}'.format(executable)
73 expected_env = '#!{}'.format(executable)
74 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(
74 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(
75 vcsserver.__version__)
75 vcsserver.__version__)
76 assert content.strip().startswith(expected_env)
76 assert content.strip().startswith(expected_env)
77 assert expected_rc_version in content
77 assert expected_rc_version in content
78
78
79 def _create_fake_hook(self, file_path, content):
79 def _create_fake_hook(self, file_path, content):
80 with open(file_path, 'w') as hook_file:
80 with open(file_path, 'w') as hook_file:
81 hook_file.write(content)
81 hook_file.write(content)
82
82
83 def create_dummy_repo(self, repo_type):
83 def create_dummy_repo(self, repo_type):
84 tmpdir = tempfile.mkdtemp()
84 tmpdir = tempfile.mkdtemp()
85 repo = AttributeDict()
85 repo = AttributeDict()
86 if repo_type == 'git':
86 if repo_type == 'git':
87 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
87 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
88 os.makedirs(repo.path)
88 os.makedirs(repo.path)
89 os.makedirs(os.path.join(repo.path, 'hooks'))
89 os.makedirs(os.path.join(repo.path, 'hooks'))
90 repo.bare = True
90 repo.bare = True
91
91
92 elif repo_type == 'svn':
92 elif repo_type == 'svn':
93 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
93 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
94 os.makedirs(repo.path)
94 os.makedirs(repo.path)
95 os.makedirs(os.path.join(repo.path, 'hooks'))
95 os.makedirs(os.path.join(repo.path, 'hooks'))
96
96
97 return repo
97 return repo
98
98
99 def check_hooks(self, repo_path, repo_bare=True):
99 def check_hooks(self, repo_path, repo_bare=True):
100 for file_name in self.HOOK_FILES:
100 for file_name in self.HOOK_FILES:
101 if repo_bare:
101 if repo_bare:
102 file_path = os.path.join(repo_path, 'hooks', file_name)
102 file_path = os.path.join(repo_path, 'hooks', file_name)
103 else:
103 else:
104 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
104 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
105 self._check_hook_file_mode(file_path)
105 self._check_hook_file_mode(file_path)
106 self._check_hook_file_content(file_path, sys.executable)
106 self._check_hook_file_content(file_path, sys.executable)
107
107
108
108
109 class TestInstallGitHooks(BaseInstallHooks):
109 class TestInstallGitHooks(BaseInstallHooks):
110 HOOK_FILES = ('pre-receive', 'post-receive')
110 HOOK_FILES = ('pre-receive', 'post-receive')
111
111
112 def test_hooks_are_installed(self):
112 def test_hooks_are_installed(self):
113 repo = self.create_dummy_repo('git')
113 repo = self.create_dummy_repo('git')
114 result = hook_utils.install_git_hooks(repo.path, repo.bare)
114 result = hook_utils.install_git_hooks(repo.path, repo.bare)
115 assert result
115 assert result
116 self.check_hooks(repo.path, repo.bare)
116 self.check_hooks(repo.path, repo.bare)
117
117
118 def test_hooks_are_replaced(self):
118 def test_hooks_are_replaced(self):
119 repo = self.create_dummy_repo('git')
119 repo = self.create_dummy_repo('git')
120 hooks_path = os.path.join(repo.path, 'hooks')
120 hooks_path = os.path.join(repo.path, 'hooks')
121 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
121 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
122 self._create_fake_hook(
122 self._create_fake_hook(
123 file_path, content="RC_HOOK_VER = 'abcde'\n")
123 file_path, content="RC_HOOK_VER = 'abcde'\n")
124
124
125 result = hook_utils.install_git_hooks(repo.path, repo.bare)
125 result = hook_utils.install_git_hooks(repo.path, repo.bare)
126 assert result
126 assert result
127 self.check_hooks(repo.path, repo.bare)
127 self.check_hooks(repo.path, repo.bare)
128
128
129 def test_non_rc_hooks_are_not_replaced(self):
129 def test_non_rc_hooks_are_not_replaced(self):
130 repo = self.create_dummy_repo('git')
130 repo = self.create_dummy_repo('git')
131 hooks_path = os.path.join(repo.path, 'hooks')
131 hooks_path = os.path.join(repo.path, 'hooks')
132 non_rc_content = 'echo "non rc hook"\n'
132 non_rc_content = 'echo "non rc hook"\n'
133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 self._create_fake_hook(
134 self._create_fake_hook(
135 file_path, content=non_rc_content)
135 file_path, content=non_rc_content)
136
136
137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 assert result
138 assert result
139
139
140 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
140 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
141 with open(file_path, 'rt') as hook_file:
141 with open(file_path, 'rt') as hook_file:
142 content = hook_file.read()
142 content = hook_file.read()
143 assert content == non_rc_content
143 assert content == non_rc_content
144
144
145 def test_non_rc_hooks_are_replaced_with_force_flag(self):
145 def test_non_rc_hooks_are_replaced_with_force_flag(self):
146 repo = self.create_dummy_repo('git')
146 repo = self.create_dummy_repo('git')
147 hooks_path = os.path.join(repo.path, 'hooks')
147 hooks_path = os.path.join(repo.path, 'hooks')
148 non_rc_content = 'echo "non rc hook"\n'
148 non_rc_content = 'echo "non rc hook"\n'
149 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
149 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
150 self._create_fake_hook(
150 self._create_fake_hook(
151 file_path, content=non_rc_content)
151 file_path, content=non_rc_content)
152
152
153 result = hook_utils.install_git_hooks(
153 result = hook_utils.install_git_hooks(
154 repo.path, repo.bare, force_create=True)
154 repo.path, repo.bare, force_create=True)
155 assert result
155 assert result
156 self.check_hooks(repo.path, repo.bare)
156 self.check_hooks(repo.path, repo.bare)
157
157
158
158
159 class TestInstallSvnHooks(BaseInstallHooks):
159 class TestInstallSvnHooks(BaseInstallHooks):
160 HOOK_FILES = ('pre-commit', 'post-commit')
160 HOOK_FILES = ('pre-commit', 'post-commit')
161
161
162 def test_hooks_are_installed(self):
162 def test_hooks_are_installed(self):
163 repo = self.create_dummy_repo('svn')
163 repo = self.create_dummy_repo('svn')
164 result = hook_utils.install_svn_hooks(repo.path)
164 result = hook_utils.install_svn_hooks(repo.path)
165 assert result
165 assert result
166 self.check_hooks(repo.path)
166 self.check_hooks(repo.path)
167
167
168 def test_hooks_are_replaced(self):
168 def test_hooks_are_replaced(self):
169 repo = self.create_dummy_repo('svn')
169 repo = self.create_dummy_repo('svn')
170 hooks_path = os.path.join(repo.path, 'hooks')
170 hooks_path = os.path.join(repo.path, 'hooks')
171 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
171 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
172 self._create_fake_hook(
172 self._create_fake_hook(
173 file_path, content="RC_HOOK_VER = 'abcde'\n")
173 file_path, content="RC_HOOK_VER = 'abcde'\n")
174
174
175 result = hook_utils.install_svn_hooks(repo.path)
175 result = hook_utils.install_svn_hooks(repo.path)
176 assert result
176 assert result
177 self.check_hooks(repo.path)
177 self.check_hooks(repo.path)
178
178
179 def test_non_rc_hooks_are_not_replaced(self):
179 def test_non_rc_hooks_are_not_replaced(self):
180 repo = self.create_dummy_repo('svn')
180 repo = self.create_dummy_repo('svn')
181 hooks_path = os.path.join(repo.path, 'hooks')
181 hooks_path = os.path.join(repo.path, 'hooks')
182 non_rc_content = 'echo "non rc hook"\n'
182 non_rc_content = 'echo "non rc hook"\n'
183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 self._create_fake_hook(
184 self._create_fake_hook(
185 file_path, content=non_rc_content)
185 file_path, content=non_rc_content)
186
186
187 result = hook_utils.install_svn_hooks(repo.path)
187 result = hook_utils.install_svn_hooks(repo.path)
188 assert result
188 assert result
189
189
190 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
190 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
191 with open(file_path, 'rt') as hook_file:
191 with open(file_path, 'rt') as hook_file:
192 content = hook_file.read()
192 content = hook_file.read()
193 assert content == non_rc_content
193 assert content == non_rc_content
194
194
195 def test_non_rc_hooks_are_replaced_with_force_flag(self):
195 def test_non_rc_hooks_are_replaced_with_force_flag(self):
196 repo = self.create_dummy_repo('svn')
196 repo = self.create_dummy_repo('svn')
197 hooks_path = os.path.join(repo.path, 'hooks')
197 hooks_path = os.path.join(repo.path, 'hooks')
198 non_rc_content = 'echo "non rc hook"\n'
198 non_rc_content = 'echo "non rc hook"\n'
199 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
199 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
200 self._create_fake_hook(
200 self._create_fake_hook(
201 file_path, content=non_rc_content)
201 file_path, content=non_rc_content)
202
202
203 result = hook_utils.install_svn_hooks(
203 result = hook_utils.install_svn_hooks(
204 repo.path, force_create=True)
204 repo.path, force_create=True)
205 assert result
205 assert result
206 self.check_hooks(repo.path, )
206 self.check_hooks(repo.path, )
@@ -1,57 +1,57 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import mock
18 import mock
19 import pytest
19 import pytest
20
20
21 from vcsserver import http_main
21 from vcsserver import http_main
22 from vcsserver.base import obfuscate_qs
22 from vcsserver.base import obfuscate_qs
23
23
24
24
25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
28 http_main.main({})
28 http_main.main({})
29 patch_largefiles_capabilities.assert_called_once_with()
29 patch_largefiles_capabilities.assert_called_once_with()
30
30
31
31
32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
34 @mock.patch(
34 @mock.patch(
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
36 mock.Mock(side_effect=Exception("Must not be called")))
36 mock.Mock(side_effect=Exception("Must not be called")))
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
38 http_main.main({})
38 http_main.main({})
39
39
40
40
41 @pytest.mark.parametrize('given, expected', [
41 @pytest.mark.parametrize('given, expected', [
42 ('bad', 'bad'),
42 ('bad', 'bad'),
43 ('query&foo=bar', 'query&foo=bar'),
43 ('query&foo=bar', 'query&foo=bar'),
44 ('equery&auth_token=bar', 'equery&auth_token=*****'),
44 ('equery&auth_token=bar', 'equery&auth_token=*****'),
45 ('a;b;c;query&foo=bar&auth_token=secret',
45 ('a;b;c;query&foo=bar&auth_token=secret',
46 'a&b&c&query&foo=bar&auth_token=*****'),
46 'a&b&c&query&foo=bar&auth_token=*****'),
47 ('', ''),
47 ('', ''),
48 (None, None),
48 (None, None),
49 ('foo=bar', 'foo=bar'),
49 ('foo=bar', 'foo=bar'),
50 ('auth_token=secret', 'auth_token=*****'),
50 ('auth_token=secret', 'auth_token=*****'),
51 ('auth_token=secret&api_key=secret2',
51 ('auth_token=secret&api_key=secret2',
52 'auth_token=*****&api_key=*****'),
52 'auth_token=*****&api_key=*****'),
53 ('auth_token=secret&api_key=secret2&param=value',
53 ('auth_token=secret&api_key=secret2&param=value',
54 'auth_token=*****&api_key=*****&param=value'),
54 'auth_token=*****&api_key=*****&param=value'),
55 ])
55 ])
56 def test_obfuscate_qs(given, expected):
56 def test_obfuscate_qs(given, expected):
57 assert expected == obfuscate_qs(given)
57 assert expected == obfuscate_qs(given)
@@ -1,249 +1,249 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19
19
20 import dulwich.protocol
20 import dulwich.protocol
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import webob
23 import webob
24 import webtest
24 import webtest
25
25
26 from vcsserver import hooks, pygrack
26 from vcsserver import hooks, pygrack
27
27
28 # pylint: disable=redefined-outer-name,protected-access
28 # pylint: disable=redefined-outer-name,protected-access
29
29
30
30
31 @pytest.fixture()
31 @pytest.fixture()
32 def pygrack_instance(tmpdir):
32 def pygrack_instance(tmpdir):
33 """
33 """
34 Creates a pygrack app instance.
34 Creates a pygrack app instance.
35
35
36 Right now, it does not much helpful regarding the passed directory.
36 Right now, it does not much helpful regarding the passed directory.
37 It just contains the required folders to pass the signature test.
37 It just contains the required folders to pass the signature test.
38 """
38 """
39 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
39 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
40 tmpdir.mkdir(dir_name)
40 tmpdir.mkdir(dir_name)
41
41
42 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
42 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
43
43
44
44
45 @pytest.fixture()
45 @pytest.fixture()
46 def pygrack_app(pygrack_instance):
46 def pygrack_app(pygrack_instance):
47 """
47 """
48 Creates a pygrack app wrapped in webtest.TestApp.
48 Creates a pygrack app wrapped in webtest.TestApp.
49 """
49 """
50 return webtest.TestApp(pygrack_instance)
50 return webtest.TestApp(pygrack_instance)
51
51
52
52
53 def test_invalid_service_info_refs_returns_403(pygrack_app):
53 def test_invalid_service_info_refs_returns_403(pygrack_app):
54 response = pygrack_app.get('/info/refs?service=git-upload-packs',
54 response = pygrack_app.get('/info/refs?service=git-upload-packs',
55 expect_errors=True)
55 expect_errors=True)
56
56
57 assert response.status_int == 403
57 assert response.status_int == 403
58
58
59
59
60 def test_invalid_endpoint_returns_403(pygrack_app):
60 def test_invalid_endpoint_returns_403(pygrack_app):
61 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
61 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
62
62
63 assert response.status_int == 403
63 assert response.status_int == 403
64
64
65
65
66 @pytest.mark.parametrize('sideband', [
66 @pytest.mark.parametrize('sideband', [
67 'side-band-64k',
67 'side-band-64k',
68 'side-band',
68 'side-band',
69 'side-band no-progress',
69 'side-band no-progress',
70 ])
70 ])
71 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
71 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
72 request = ''.join([
72 request = ''.join([
73 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
73 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
74 'multi_ack %s ofs-delta\n' % sideband,
74 'multi_ack %s ofs-delta\n' % sideband,
75 '0000',
75 '0000',
76 '0009done\n',
76 '0009done\n',
77 ])
77 ])
78 with mock.patch('vcsserver.hooks.git_pre_pull',
78 with mock.patch('vcsserver.hooks.git_pre_pull',
79 return_value=hooks.HookResponse(1, 'foo')):
79 return_value=hooks.HookResponse(1, 'foo')):
80 response = pygrack_app.post(
80 response = pygrack_app.post(
81 '/git-upload-pack', params=request,
81 '/git-upload-pack', params=request,
82 content_type='application/x-git-upload-pack')
82 content_type='application/x-git-upload-pack')
83
83
84 data = io.BytesIO(response.body)
84 data = io.BytesIO(response.body)
85 proto = dulwich.protocol.Protocol(data.read, None)
85 proto = dulwich.protocol.Protocol(data.read, None)
86 packets = list(proto.read_pkt_seq())
86 packets = list(proto.read_pkt_seq())
87
87
88 expected_packets = [
88 expected_packets = [
89 'NAK\n', '\x02foo', '\x02Pre pull hook failed: aborting\n',
89 'NAK\n', '\x02foo', '\x02Pre pull hook failed: aborting\n',
90 '\x01' + pygrack.GitRepository.EMPTY_PACK,
90 '\x01' + pygrack.GitRepository.EMPTY_PACK,
91 ]
91 ]
92 assert packets == expected_packets
92 assert packets == expected_packets
93
93
94
94
95 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
95 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
96 request = ''.join([
96 request = ''.join([
97 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
97 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
98 'multi_ack ofs-delta\n'
98 'multi_ack ofs-delta\n'
99 '0000',
99 '0000',
100 '0009done\n',
100 '0009done\n',
101 ])
101 ])
102 with mock.patch('vcsserver.hooks.git_pre_pull',
102 with mock.patch('vcsserver.hooks.git_pre_pull',
103 return_value=hooks.HookResponse(1, 'foo')):
103 return_value=hooks.HookResponse(1, 'foo')):
104 response = pygrack_app.post(
104 response = pygrack_app.post(
105 '/git-upload-pack', params=request,
105 '/git-upload-pack', params=request,
106 content_type='application/x-git-upload-pack')
106 content_type='application/x-git-upload-pack')
107
107
108 assert response.body == pygrack.GitRepository.EMPTY_PACK
108 assert response.body == pygrack.GitRepository.EMPTY_PACK
109
109
110
110
111 def test_pull_has_hook_messages(pygrack_app):
111 def test_pull_has_hook_messages(pygrack_app):
112 request = ''.join([
112 request = ''.join([
113 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
113 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
114 'multi_ack side-band-64k ofs-delta\n'
114 'multi_ack side-band-64k ofs-delta\n'
115 '0000',
115 '0000',
116 '0009done\n',
116 '0009done\n',
117 ])
117 ])
118 with mock.patch('vcsserver.hooks.git_pre_pull',
118 with mock.patch('vcsserver.hooks.git_pre_pull',
119 return_value=hooks.HookResponse(0, 'foo')):
119 return_value=hooks.HookResponse(0, 'foo')):
120 with mock.patch('vcsserver.hooks.git_post_pull',
120 with mock.patch('vcsserver.hooks.git_post_pull',
121 return_value=hooks.HookResponse(1, 'bar')):
121 return_value=hooks.HookResponse(1, 'bar')):
122 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
122 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
123 return_value=['0008NAK\n0009subp\n0000']):
123 return_value=['0008NAK\n0009subp\n0000']):
124 response = pygrack_app.post(
124 response = pygrack_app.post(
125 '/git-upload-pack', params=request,
125 '/git-upload-pack', params=request,
126 content_type='application/x-git-upload-pack')
126 content_type='application/x-git-upload-pack')
127
127
128 data = io.BytesIO(response.body)
128 data = io.BytesIO(response.body)
129 proto = dulwich.protocol.Protocol(data.read, None)
129 proto = dulwich.protocol.Protocol(data.read, None)
130 packets = list(proto.read_pkt_seq())
130 packets = list(proto.read_pkt_seq())
131
131
132 assert packets == ['NAK\n', '\x02foo', 'subp\n', '\x02bar']
132 assert packets == ['NAK\n', '\x02foo', 'subp\n', '\x02bar']
133
133
134
134
135 def test_get_want_capabilities(pygrack_instance):
135 def test_get_want_capabilities(pygrack_instance):
136 data = io.BytesIO(
136 data = io.BytesIO(
137 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
137 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
138 'multi_ack side-band-64k ofs-delta\n00000009done\n')
138 'multi_ack side-band-64k ofs-delta\n00000009done\n')
139
139
140 request = webob.Request({
140 request = webob.Request({
141 'wsgi.input': data,
141 'wsgi.input': data,
142 'REQUEST_METHOD': 'POST',
142 'REQUEST_METHOD': 'POST',
143 'webob.is_body_seekable': True
143 'webob.is_body_seekable': True
144 })
144 })
145
145
146 capabilities = pygrack_instance._get_want_capabilities(request)
146 capabilities = pygrack_instance._get_want_capabilities(request)
147
147
148 assert capabilities == frozenset(
148 assert capabilities == frozenset(
149 ('ofs-delta', 'multi_ack', 'side-band-64k'))
149 ('ofs-delta', 'multi_ack', 'side-band-64k'))
150 assert data.tell() == 0
150 assert data.tell() == 0
151
151
152
152
153 @pytest.mark.parametrize('data,capabilities,expected', [
153 @pytest.mark.parametrize('data,capabilities,expected', [
154 ('foo', [], []),
154 ('foo', [], []),
155 ('', ['side-band-64k'], []),
155 ('', ['side-band-64k'], []),
156 ('', ['side-band'], []),
156 ('', ['side-band'], []),
157 ('foo', ['side-band-64k'], ['0008\x02foo']),
157 ('foo', ['side-band-64k'], ['0008\x02foo']),
158 ('foo', ['side-band'], ['0008\x02foo']),
158 ('foo', ['side-band'], ['0008\x02foo']),
159 ('f'*1000, ['side-band-64k'], ['03ed\x02' + 'f' * 1000]),
159 ('f'*1000, ['side-band-64k'], ['03ed\x02' + 'f' * 1000]),
160 ('f'*1000, ['side-band'], ['03e8\x02' + 'f' * 995, '000a\x02fffff']),
160 ('f'*1000, ['side-band'], ['03e8\x02' + 'f' * 995, '000a\x02fffff']),
161 ('f'*65520, ['side-band-64k'], ['fff0\x02' + 'f' * 65515, '000a\x02fffff']),
161 ('f'*65520, ['side-band-64k'], ['fff0\x02' + 'f' * 65515, '000a\x02fffff']),
162 ('f'*65520, ['side-band'], ['03e8\x02' + 'f' * 995] * 65 + ['0352\x02' + 'f' * 845]),
162 ('f'*65520, ['side-band'], ['03e8\x02' + 'f' * 995] * 65 + ['0352\x02' + 'f' * 845]),
163 ], ids=[
163 ], ids=[
164 'foo-empty',
164 'foo-empty',
165 'empty-64k', 'empty',
165 'empty-64k', 'empty',
166 'foo-64k', 'foo',
166 'foo-64k', 'foo',
167 'f-1000-64k', 'f-1000',
167 'f-1000-64k', 'f-1000',
168 'f-65520-64k', 'f-65520'])
168 'f-65520-64k', 'f-65520'])
169 def test_get_messages(pygrack_instance, data, capabilities, expected):
169 def test_get_messages(pygrack_instance, data, capabilities, expected):
170 messages = pygrack_instance._get_messages(data, capabilities)
170 messages = pygrack_instance._get_messages(data, capabilities)
171
171
172 assert messages == expected
172 assert messages == expected
173
173
174
174
175 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
175 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
176 # Unexpected response
176 # Unexpected response
177 ('unexpected_response', ['side-band-64k'], 'foo', 'bar'),
177 ('unexpected_response', ['side-band-64k'], 'foo', 'bar'),
178 # No sideband
178 # No sideband
179 ('no-sideband', [], 'foo', 'bar'),
179 ('no-sideband', [], 'foo', 'bar'),
180 # No messages
180 # No messages
181 ('no-messages', ['side-band-64k'], '', ''),
181 ('no-messages', ['side-band-64k'], '', ''),
182 ])
182 ])
183 def test_inject_messages_to_response_nothing_to_do(
183 def test_inject_messages_to_response_nothing_to_do(
184 pygrack_instance, response, capabilities, pre_pull_messages,
184 pygrack_instance, response, capabilities, pre_pull_messages,
185 post_pull_messages):
185 post_pull_messages):
186 new_response = pygrack_instance._inject_messages_to_response(
186 new_response = pygrack_instance._inject_messages_to_response(
187 response, capabilities, pre_pull_messages, post_pull_messages)
187 response, capabilities, pre_pull_messages, post_pull_messages)
188
188
189 assert new_response == response
189 assert new_response == response
190
190
191
191
192 @pytest.mark.parametrize('capabilities', [
192 @pytest.mark.parametrize('capabilities', [
193 ['side-band'],
193 ['side-band'],
194 ['side-band-64k'],
194 ['side-band-64k'],
195 ])
195 ])
196 def test_inject_messages_to_response_single_element(pygrack_instance,
196 def test_inject_messages_to_response_single_element(pygrack_instance,
197 capabilities):
197 capabilities):
198 response = ['0008NAK\n0009subp\n0000']
198 response = ['0008NAK\n0009subp\n0000']
199 new_response = pygrack_instance._inject_messages_to_response(
199 new_response = pygrack_instance._inject_messages_to_response(
200 response, capabilities, 'foo', 'bar')
200 response, capabilities, 'foo', 'bar')
201
201
202 expected_response = [
202 expected_response = [
203 '0008NAK\n', '0008\x02foo', '0009subp\n', '0008\x02bar', '0000']
203 '0008NAK\n', '0008\x02foo', '0009subp\n', '0008\x02bar', '0000']
204
204
205 assert new_response == expected_response
205 assert new_response == expected_response
206
206
207
207
208 @pytest.mark.parametrize('capabilities', [
208 @pytest.mark.parametrize('capabilities', [
209 ['side-band'],
209 ['side-band'],
210 ['side-band-64k'],
210 ['side-band-64k'],
211 ])
211 ])
212 def test_inject_messages_to_response_multi_element(pygrack_instance,
212 def test_inject_messages_to_response_multi_element(pygrack_instance,
213 capabilities):
213 capabilities):
214 response = [
214 response = [
215 '0008NAK\n000asubp1\n', '000asubp2\n', '000asubp3\n', '000asubp4\n0000']
215 '0008NAK\n000asubp1\n', '000asubp2\n', '000asubp3\n', '000asubp4\n0000']
216 new_response = pygrack_instance._inject_messages_to_response(
216 new_response = pygrack_instance._inject_messages_to_response(
217 response, capabilities, 'foo', 'bar')
217 response, capabilities, 'foo', 'bar')
218
218
219 expected_response = [
219 expected_response = [
220 '0008NAK\n', '0008\x02foo', '000asubp1\n', '000asubp2\n', '000asubp3\n',
220 '0008NAK\n', '0008\x02foo', '000asubp1\n', '000asubp2\n', '000asubp3\n',
221 '000asubp4\n', '0008\x02bar', '0000'
221 '000asubp4\n', '0008\x02bar', '0000'
222 ]
222 ]
223
223
224 assert new_response == expected_response
224 assert new_response == expected_response
225
225
226
226
227 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
227 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
228 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
228 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
229
229
230 assert response == [pygrack.GitRepository.EMPTY_PACK]
230 assert response == [pygrack.GitRepository.EMPTY_PACK]
231
231
232
232
233 @pytest.mark.parametrize('capabilities', [
233 @pytest.mark.parametrize('capabilities', [
234 ['side-band'],
234 ['side-band'],
235 ['side-band-64k'],
235 ['side-band-64k'],
236 ['side-band-64k', 'no-progress'],
236 ['side-band-64k', 'no-progress'],
237 ])
237 ])
238 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
238 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
239 response = pygrack_instance._build_failed_pre_pull_response(
239 response = pygrack_instance._build_failed_pre_pull_response(
240 capabilities, 'foo')
240 capabilities, 'foo')
241
241
242 expected_response = [
242 expected_response = [
243 '0008NAK\n', '0008\x02foo', '0024\x02Pre pull hook failed: aborting\n',
243 '0008NAK\n', '0008\x02foo', '0024\x02Pre pull hook failed: aborting\n',
244 '%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5,
244 '%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5,
245 pygrack.GitRepository.EMPTY_PACK),
245 pygrack.GitRepository.EMPTY_PACK),
246 '0000',
246 '0000',
247 ]
247 ]
248
248
249 assert response == expected_response
249 assert response == expected_response
@@ -1,86 +1,86 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19
19
20 import mercurial.hg
20 import mercurial.hg
21 import mercurial.ui
21 import mercurial.ui
22 import mercurial.error
22 import mercurial.error
23 import mock
23 import mock
24 import pytest
24 import pytest
25 import webtest
25 import webtest
26
26
27 from vcsserver import scm_app
27 from vcsserver import scm_app
28
28
29
29
30 def test_hg_does_not_accept_invalid_cmd(tmpdir):
30 def test_hg_does_not_accept_invalid_cmd(tmpdir):
31 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
31 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
32 app = webtest.TestApp(scm_app.HgWeb(repo))
32 app = webtest.TestApp(scm_app.HgWeb(repo))
33
33
34 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
34 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
35
35
36 assert response.status_int == 400
36 assert response.status_int == 400
37
37
38
38
39 def test_create_hg_wsgi_app_requirement_error(tmpdir):
39 def test_create_hg_wsgi_app_requirement_error(tmpdir):
40 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
40 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
41 config = (
41 config = (
42 ('paths', 'default', ''),
42 ('paths', 'default', ''),
43 )
43 )
44 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
44 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
45 hgweb_mock.side_effect = mercurial.error.RequirementError()
45 hgweb_mock.side_effect = mercurial.error.RequirementError()
46 with pytest.raises(Exception):
46 with pytest.raises(Exception):
47 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
47 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
48
48
49
49
50 def test_git_returns_not_found(tmpdir):
50 def test_git_returns_not_found(tmpdir):
51 app = webtest.TestApp(
51 app = webtest.TestApp(
52 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
52 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
53
53
54 response = app.get('/repo_name/inforefs?service=git-upload-pack',
54 response = app.get('/repo_name/inforefs?service=git-upload-pack',
55 expect_errors=True)
55 expect_errors=True)
56
56
57 assert response.status_int == 404
57 assert response.status_int == 404
58
58
59
59
60 def test_git(tmpdir):
60 def test_git(tmpdir):
61 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
61 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
62 tmpdir.mkdir(dir_name)
62 tmpdir.mkdir(dir_name)
63
63
64 app = webtest.TestApp(
64 app = webtest.TestApp(
65 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
65 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
66
66
67 # We set service to git-upload-packs to trigger a 403
67 # We set service to git-upload-packs to trigger a 403
68 response = app.get('/repo_name/inforefs?service=git-upload-packs',
68 response = app.get('/repo_name/inforefs?service=git-upload-packs',
69 expect_errors=True)
69 expect_errors=True)
70
70
71 assert response.status_int == 403
71 assert response.status_int == 403
72
72
73
73
74 def test_git_fallbacks_to_git_folder(tmpdir):
74 def test_git_fallbacks_to_git_folder(tmpdir):
75 tmpdir.mkdir('.git')
75 tmpdir.mkdir('.git')
76 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
76 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
77 tmpdir.mkdir(os.path.join('.git', dir_name))
77 tmpdir.mkdir(os.path.join('.git', dir_name))
78
78
79 app = webtest.TestApp(
79 app = webtest.TestApp(
80 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
80 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
81
81
82 # We set service to git-upload-packs to trigger a 403
82 # We set service to git-upload-packs to trigger a 403
83 response = app.get('/repo_name/inforefs?service=git-upload-packs',
83 response = app.get('/repo_name/inforefs?service=git-upload-packs',
84 expect_errors=True)
84 expect_errors=True)
85
85
86 assert response.status_int == 403
86 assert response.status_int == 403
@@ -1,39 +1,39 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import os
18 import os
19
19
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 from vcsserver.server import VcsServer
23 from vcsserver.server import VcsServer
24
24
25
25
26 def test_provides_the_pid(server):
26 def test_provides_the_pid(server):
27 pid = server.get_pid()
27 pid = server.get_pid()
28 assert pid == os.getpid()
28 assert pid == os.getpid()
29
29
30
30
31 def test_allows_to_trigger_the_garbage_collector(server):
31 def test_allows_to_trigger_the_garbage_collector(server):
32 with mock.patch('gc.collect') as collect:
32 with mock.patch('gc.collect') as collect:
33 server.run_gc()
33 server.run_gc()
34 assert collect.called
34 assert collect.called
35
35
36
36
37 @pytest.fixture
37 @pytest.fixture
38 def server():
38 def server():
39 return VcsServer()
39 return VcsServer()
@@ -1,132 +1,155 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import os
19 import os
20 import sys
20 import sys
21
21
22 import pytest
22 import pytest
23
23
24 from vcsserver import subprocessio
24 from vcsserver import subprocessio
25
25
26
26
27 class KindaFilelike(object): # pragma: no cover
28
29 def __init__(self, data, size):
30 chunks = size / len(data)
31
32 self.stream = self._get_stream(data, chunks)
33
34 def _get_stream(self, data, chunks):
35 for x in xrange(chunks):
36 yield data
37
38 def read(self, n):
39
40 buffer_stream = ''
41 for chunk in self.stream:
42 buffer_stream += chunk
43 if len(buffer_stream) >= n:
44 break
45
46 # self.stream = self.bytes[n:]
47 return buffer_stream
48
49
27 @pytest.fixture(scope='module')
50 @pytest.fixture(scope='module')
28 def environ():
51 def environ():
29 """Delete coverage variables, as they make the tests fail."""
52 """Delete coverage variables, as they make the tests fail."""
30 env = dict(os.environ)
53 env = dict(os.environ)
31 for key in env.keys():
54 for key in env.keys():
32 if key.startswith('COV_CORE_'):
55 if key.startswith('COV_CORE_'):
33 del env[key]
56 del env[key]
34
57
35 return env
58 return env
36
59
37
60
38 def _get_python_args(script):
61 def _get_python_args(script):
39 return [sys.executable, '-c', 'import sys; import time; import shutil; ' + script]
62 return [sys.executable, '-c', 'import sys; import time; import shutil; ' + script]
40
63
41
64
42 def test_raise_exception_on_non_zero_return_code(environ):
65 def test_raise_exception_on_non_zero_return_code(environ):
43 args = _get_python_args('sys.exit(1)')
66 args = _get_python_args('sys.exit(1)')
44 with pytest.raises(EnvironmentError):
67 with pytest.raises(EnvironmentError):
45 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
68 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
46
69
47
70
48 def test_does_not_fail_on_non_zero_return_code(environ):
71 def test_does_not_fail_on_non_zero_return_code(environ):
49 args = _get_python_args('sys.exit(1)')
72 args = _get_python_args('sys.exit(1)')
50 output = ''.join(
73 output = ''.join(
51 subprocessio.SubprocessIOChunker(
74 subprocessio.SubprocessIOChunker(
52 args, shell=False, fail_on_return_code=False, env=environ
75 args, shell=False, fail_on_return_code=False, env=environ
53 )
76 )
54 )
77 )
55
78
56 assert output == ''
79 assert output == ''
57
80
58
81
59 def test_raise_exception_on_stderr(environ):
82 def test_raise_exception_on_stderr(environ):
60 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
83 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
61 with pytest.raises(EnvironmentError) as excinfo:
84 with pytest.raises(EnvironmentError) as excinfo:
62 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
85 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
63
86
64 assert 'exited due to an error:\nX' in str(excinfo.value)
87 assert 'exited due to an error:\nX' in str(excinfo.value)
65
88
66
89
67 def test_does_not_fail_on_stderr(environ):
90 def test_does_not_fail_on_stderr(environ):
68 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
91 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
69 output = ''.join(
92 output = ''.join(
70 subprocessio.SubprocessIOChunker(
93 subprocessio.SubprocessIOChunker(
71 args, shell=False, fail_on_stderr=False, env=environ
94 args, shell=False, fail_on_stderr=False, env=environ
72 )
95 )
73 )
96 )
74
97
75 assert output == ''
98 assert output == ''
76
99
77
100
78 @pytest.mark.parametrize('size', [1, 10 ** 5])
101 @pytest.mark.parametrize('size', [1, 10 ** 5])
79 def test_output_with_no_input(size, environ):
102 def test_output_with_no_input(size, environ):
80 print(type(environ))
103 print(type(environ))
81 data = 'X'
104 data = 'X'
82 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
105 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
83 output = ''.join(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
106 output = ''.join(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
84
107
85 assert output == data * size
108 assert output == data * size
86
109
87
110
88 @pytest.mark.parametrize('size', [1, 10 ** 5])
111 @pytest.mark.parametrize('size', [1, 10 ** 5])
89 def test_output_with_no_input_does_not_fail(size, environ):
112 def test_output_with_no_input_does_not_fail(size, environ):
90 data = 'X'
113 data = 'X'
91 args = _get_python_args('sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
114 args = _get_python_args('sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
92 output = ''.join(
115 output = ''.join(
93 subprocessio.SubprocessIOChunker(
116 subprocessio.SubprocessIOChunker(
94 args, shell=False, fail_on_return_code=False, env=environ
117 args, shell=False, fail_on_return_code=False, env=environ
95 )
118 )
96 )
119 )
97
120
98 print("{} {}".format(len(data * size), len(output)))
121 print("{} {}".format(len(data * size), len(output)))
99 assert output == data * size
122 assert output == data * size
100
123
101
124
102 @pytest.mark.parametrize('size', [1, 10 ** 5])
125 @pytest.mark.parametrize('size', [1, 10 ** 5])
103 def test_output_with_input(size, environ):
126 def test_output_with_input(size, environ):
104 data = 'X' * size
127 data_len = size
105 inputstream = io.BytesIO(data)
128 inputstream = KindaFilelike('X', size)
129
106 # This acts like the cat command.
130 # This acts like the cat command.
107 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
131 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
108 output = ''.join(
132 output = ''.join(
109 subprocessio.SubprocessIOChunker(
133 subprocessio.SubprocessIOChunker(
110 args, shell=False, inputstream=inputstream, env=environ
134 args, shell=False, inputstream=inputstream, env=environ
111 )
135 )
112 )
136 )
113
137
114 print("{} {}".format(len(data * size), len(output)))
138 assert len(output) == data_len
115 assert output == data
116
139
117
140
118 @pytest.mark.parametrize('size', [1, 10 ** 5])
141 @pytest.mark.parametrize('size', [1, 10 ** 5])
119 def test_output_with_input_skipping_iterator(size, environ):
142 def test_output_with_input_skipping_iterator(size, environ):
120 data = 'X' * size
143 data_len = size
121 inputstream = io.BytesIO(data)
144 inputstream = KindaFilelike('X', size)
145
122 # This acts like the cat command.
146 # This acts like the cat command.
123 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
147 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
124
148
125 # Note: assigning the chunker makes sure that it is not deleted too early
149 # Note: assigning the chunker makes sure that it is not deleted too early
126 chunker = subprocessio.SubprocessIOChunker(
150 chunker = subprocessio.SubprocessIOChunker(
127 args, shell=False, inputstream=inputstream, env=environ
151 args, shell=False, inputstream=inputstream, env=environ
128 )
152 )
129 output = ''.join(chunker.output)
153 output = ''.join(chunker.output)
130
154
131 print("{} {}".format(len(data * size), len(output)))
155 assert len(output) == data_len
132 assert output == data
@@ -1,82 +1,82 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import mock
19 import mock
20 import pytest
20 import pytest
21 import sys
21 import sys
22
22
23
23
24 class MockPopen(object):
24 class MockPopen(object):
25 def __init__(self, stderr):
25 def __init__(self, stderr):
26 self.stdout = io.BytesIO('')
26 self.stdout = io.BytesIO('')
27 self.stderr = io.BytesIO(stderr)
27 self.stderr = io.BytesIO(stderr)
28 self.returncode = 1
28 self.returncode = 1
29
29
30 def wait(self):
30 def wait(self):
31 pass
31 pass
32
32
33
33
34 INVALID_CERTIFICATE_STDERR = '\n'.join([
34 INVALID_CERTIFICATE_STDERR = '\n'.join([
35 'svnrdump: E230001: Unable to connect to a repository at URL url',
35 'svnrdump: E230001: Unable to connect to a repository at URL url',
36 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
36 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
37 ])
37 ])
38
38
39
39
40 @pytest.mark.parametrize('stderr,expected_reason', [
40 @pytest.mark.parametrize('stderr,expected_reason', [
41 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
41 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
42 ('svnrdump: E123456', 'UNKNOWN:svnrdump: E123456'),
42 ('svnrdump: E123456', 'UNKNOWN:svnrdump: E123456'),
43 ], ids=['invalid-cert-stderr', 'svnrdump-err-123456'])
43 ], ids=['invalid-cert-stderr', 'svnrdump-err-123456'])
44 @pytest.mark.xfail(sys.platform == "cygwin",
44 @pytest.mark.xfail(sys.platform == "cygwin",
45 reason="SVN not packaged for Cygwin")
45 reason="SVN not packaged for Cygwin")
46 def test_import_remote_repository_certificate_error(stderr, expected_reason):
46 def test_import_remote_repository_certificate_error(stderr, expected_reason):
47 from vcsserver import svn
47 from vcsserver import svn
48
48
49 remote = svn.SvnRemote(None)
49 remote = svn.SvnRemote(None)
50 remote.is_path_valid_repository = lambda wire, path: True
50 remote.is_path_valid_repository = lambda wire, path: True
51
51
52 with mock.patch('subprocess.Popen',
52 with mock.patch('subprocess.Popen',
53 return_value=MockPopen(stderr)):
53 return_value=MockPopen(stderr)):
54 with pytest.raises(Exception) as excinfo:
54 with pytest.raises(Exception) as excinfo:
55 remote.import_remote_repository({'path': 'path'}, 'url')
55 remote.import_remote_repository({'path': 'path'}, 'url')
56
56
57 expected_error_args = (
57 expected_error_args = (
58 'Failed to dump the remote repository from url. Reason:{}'.format(expected_reason),)
58 'Failed to dump the remote repository from url. Reason:{}'.format(expected_reason),)
59
59
60 assert excinfo.value.args == expected_error_args
60 assert excinfo.value.args == expected_error_args
61
61
62
62
63 def test_svn_libraries_can_be_imported():
63 def test_svn_libraries_can_be_imported():
64 import svn
64 import svn
65 import svn.client
65 import svn.client
66 assert svn.client is not None
66 assert svn.client is not None
67
67
68
68
69 @pytest.mark.parametrize('example_url, parts', [
69 @pytest.mark.parametrize('example_url, parts', [
70 ('http://server.com', (None, None, 'http://server.com')),
70 ('http://server.com', (None, None, 'http://server.com')),
71 ('http://user@server.com', ('user', None, 'http://user@server.com')),
71 ('http://user@server.com', ('user', None, 'http://user@server.com')),
72 ('http://user:pass@server.com', ('user', 'pass', 'http://user:pass@server.com')),
72 ('http://user:pass@server.com', ('user', 'pass', 'http://user:pass@server.com')),
73 ('<script>', (None, None, '<script>')),
73 ('<script>', (None, None, '<script>')),
74 ('http://', (None, None, 'http://')),
74 ('http://', (None, None, 'http://')),
75 ])
75 ])
76 def test_username_password_extraction_from_url(example_url, parts):
76 def test_username_password_extraction_from_url(example_url, parts):
77 from vcsserver import svn
77 from vcsserver import svn
78
78
79 remote = svn.SvnRemote(None)
79 remote = svn.SvnRemote(None)
80 remote.is_path_valid_repository = lambda wire, path: True
80 remote.is_path_valid_repository = lambda wire, path: True
81
81
82 assert remote.get_url_and_credentials(example_url) == parts
82 assert remote.get_url_and_credentials(example_url) == parts
@@ -1,96 +1,96 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import wsgiref.simple_server
18 import wsgiref.simple_server
19 import wsgiref.validate
19 import wsgiref.validate
20
20
21 from vcsserver import wsgi_app_caller
21 from vcsserver import wsgi_app_caller
22
22
23
23
24 # pylint: disable=protected-access,too-many-public-methods
24 # pylint: disable=protected-access,too-many-public-methods
25
25
26
26
27 @wsgiref.validate.validator
27 @wsgiref.validate.validator
28 def demo_app(environ, start_response):
28 def demo_app(environ, start_response):
29 """WSGI app used for testing."""
29 """WSGI app used for testing."""
30 data = [
30 data = [
31 'Hello World!\n',
31 'Hello World!\n',
32 'input_data=%s\n' % environ['wsgi.input'].read(),
32 'input_data=%s\n' % environ['wsgi.input'].read(),
33 ]
33 ]
34 for key, value in sorted(environ.items()):
34 for key, value in sorted(environ.items()):
35 data.append('%s=%s\n' % (key, value))
35 data.append('%s=%s\n' % (key, value))
36
36
37 write = start_response("200 OK", [('Content-Type', 'text/plain')])
37 write = start_response("200 OK", [('Content-Type', 'text/plain')])
38 write('Old school write method\n')
38 write('Old school write method\n')
39 write('***********************\n')
39 write('***********************\n')
40 return data
40 return data
41
41
42
42
43 BASE_ENVIRON = {
43 BASE_ENVIRON = {
44 'REQUEST_METHOD': 'GET',
44 'REQUEST_METHOD': 'GET',
45 'SERVER_NAME': 'localhost',
45 'SERVER_NAME': 'localhost',
46 'SERVER_PORT': '80',
46 'SERVER_PORT': '80',
47 'SCRIPT_NAME': '',
47 'SCRIPT_NAME': '',
48 'PATH_INFO': '/',
48 'PATH_INFO': '/',
49 'QUERY_STRING': '',
49 'QUERY_STRING': '',
50 'foo.var': 'bla',
50 'foo.var': 'bla',
51 }
51 }
52
52
53
53
54 def test_complete_environ():
54 def test_complete_environ():
55 environ = dict(BASE_ENVIRON)
55 environ = dict(BASE_ENVIRON)
56 data = "data"
56 data = "data"
57 wsgi_app_caller._complete_environ(environ, data)
57 wsgi_app_caller._complete_environ(environ, data)
58 wsgiref.validate.check_environ(environ)
58 wsgiref.validate.check_environ(environ)
59
59
60 assert data == environ['wsgi.input'].read()
60 assert data == environ['wsgi.input'].read()
61
61
62
62
63 def test_start_response():
63 def test_start_response():
64 start_response = wsgi_app_caller._StartResponse()
64 start_response = wsgi_app_caller._StartResponse()
65 status = '200 OK'
65 status = '200 OK'
66 headers = [('Content-Type', 'text/plain')]
66 headers = [('Content-Type', 'text/plain')]
67 start_response(status, headers)
67 start_response(status, headers)
68
68
69 assert status == start_response.status
69 assert status == start_response.status
70 assert headers == start_response.headers
70 assert headers == start_response.headers
71
71
72
72
73 def test_start_response_with_error():
73 def test_start_response_with_error():
74 start_response = wsgi_app_caller._StartResponse()
74 start_response = wsgi_app_caller._StartResponse()
75 status = '500 Internal Server Error'
75 status = '500 Internal Server Error'
76 headers = [('Content-Type', 'text/plain')]
76 headers = [('Content-Type', 'text/plain')]
77 start_response(status, headers, (None, None, None))
77 start_response(status, headers, (None, None, None))
78
78
79 assert status == start_response.status
79 assert status == start_response.status
80 assert headers == start_response.headers
80 assert headers == start_response.headers
81
81
82
82
83 def test_wsgi_app_caller():
83 def test_wsgi_app_caller():
84 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
84 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
85 environ = dict(BASE_ENVIRON)
85 environ = dict(BASE_ENVIRON)
86 input_data = 'some text'
86 input_data = 'some text'
87 responses, status, headers = caller.handle(environ, input_data)
87 responses, status, headers = caller.handle(environ, input_data)
88 response = ''.join(responses)
88 response = ''.join(responses)
89
89
90 assert status == '200 OK'
90 assert status == '200 OK'
91 assert headers == [('Content-Type', 'text/plain')]
91 assert headers == [('Content-Type', 'text/plain')]
92 assert response.startswith(
92 assert response.startswith(
93 'Old school write method\n***********************\n')
93 'Old school write method\n***********************\n')
94 assert 'Hello World!\n' in response
94 assert 'Hello World!\n' in response
95 assert 'foo.var=bla\n' in response
95 assert 'foo.var=bla\n' in response
96 assert 'input_data=%s\n' % input_data in response
96 assert 'input_data=%s\n' % input_data in response
@@ -1,58 +1,58 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
19
19
20 import time
20 import time
21 import logging
21 import logging
22
22
23
23
24 from vcsserver.utils import safe_str
24 from vcsserver.utils import safe_str
25
25
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 def get_access_path(request):
30 def get_access_path(request):
31 environ = request.environ
31 environ = request.environ
32 return environ.get('PATH_INFO')
32 return environ.get('PATH_INFO')
33
33
34
34
35 class RequestWrapperTween(object):
35 class RequestWrapperTween(object):
36 def __init__(self, handler, registry):
36 def __init__(self, handler, registry):
37 self.handler = handler
37 self.handler = handler
38 self.registry = registry
38 self.registry = registry
39
39
40 # one-time configuration code goes here
40 # one-time configuration code goes here
41
41
42 def __call__(self, request):
42 def __call__(self, request):
43 start = time.time()
43 start = time.time()
44 try:
44 try:
45 response = self.handler(request)
45 response = self.handler(request)
46 finally:
46 finally:
47 end = time.time()
47 end = time.time()
48
48
49 log.info('IP: %s Request to path: `%s` time: %.3fs',
49 log.info('IP: %s Request to path: `%s` time: %.3fs',
50 '127.0.0.1', safe_str(get_access_path(request)), end - start)
50 '127.0.0.1', safe_str(get_access_path(request)), end - start)
51
51
52 return response
52 return response
53
53
54
54
55 def includeme(config):
55 def includeme(config):
56 config.add_tween(
56 config.add_tween(
57 'vcsserver.tweens.RequestWrapperTween',
57 'vcsserver.tweens.RequestWrapperTween',
58 )
58 )
@@ -1,89 +1,89 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 import logging
17 import logging
18 import hashlib
18 import hashlib
19
19
20 log = logging.getLogger(__name__)
20 log = logging.getLogger(__name__)
21
21
22
22
23 def safe_int(val, default=None):
23 def safe_int(val, default=None):
24 """
24 """
25 Returns int() of val if val is not convertable to int use default
25 Returns int() of val if val is not convertable to int use default
26 instead
26 instead
27
27
28 :param val:
28 :param val:
29 :param default:
29 :param default:
30 """
30 """
31
31
32 try:
32 try:
33 val = int(val)
33 val = int(val)
34 except (ValueError, TypeError):
34 except (ValueError, TypeError):
35 val = default
35 val = default
36
36
37 return val
37 return val
38
38
39
39
40 def safe_str(unicode_, to_encoding=['utf8']):
40 def safe_str(unicode_, to_encoding=['utf8']):
41 """
41 """
42 safe str function. Does few trick to turn unicode_ into string
42 safe str function. Does few trick to turn unicode_ into string
43
43
44 In case of UnicodeEncodeError, we try to return it with encoding detected
44 In case of UnicodeEncodeError, we try to return it with encoding detected
45 by chardet library if it fails fallback to string with errors replaced
45 by chardet library if it fails fallback to string with errors replaced
46
46
47 :param unicode_: unicode to encode
47 :param unicode_: unicode to encode
48 :rtype: str
48 :rtype: str
49 :returns: str object
49 :returns: str object
50 """
50 """
51
51
52 # if it's not basestr cast to str
52 # if it's not basestr cast to str
53 if not isinstance(unicode_, basestring):
53 if not isinstance(unicode_, basestring):
54 return str(unicode_)
54 return str(unicode_)
55
55
56 if isinstance(unicode_, str):
56 if isinstance(unicode_, str):
57 return unicode_
57 return unicode_
58
58
59 if not isinstance(to_encoding, (list, tuple)):
59 if not isinstance(to_encoding, (list, tuple)):
60 to_encoding = [to_encoding]
60 to_encoding = [to_encoding]
61
61
62 for enc in to_encoding:
62 for enc in to_encoding:
63 try:
63 try:
64 return unicode_.encode(enc)
64 return unicode_.encode(enc)
65 except UnicodeEncodeError:
65 except UnicodeEncodeError:
66 pass
66 pass
67
67
68 try:
68 try:
69 import chardet
69 import chardet
70 encoding = chardet.detect(unicode_)['encoding']
70 encoding = chardet.detect(unicode_)['encoding']
71 if encoding is None:
71 if encoding is None:
72 raise UnicodeEncodeError()
72 raise UnicodeEncodeError()
73
73
74 return unicode_.encode(encoding)
74 return unicode_.encode(encoding)
75 except (ImportError, UnicodeEncodeError):
75 except (ImportError, UnicodeEncodeError):
76 return unicode_.encode(to_encoding[0], 'replace')
76 return unicode_.encode(to_encoding[0], 'replace')
77
77
78
78
79 class AttributeDict(dict):
79 class AttributeDict(dict):
80 def __getattr__(self, attr):
80 def __getattr__(self, attr):
81 return self.get(attr, None)
81 return self.get(attr, None)
82 __setattr__ = dict.__setitem__
82 __setattr__ = dict.__setitem__
83 __delattr__ = dict.__delitem__
83 __delattr__ = dict.__delitem__
84
84
85
85
86 def sha1(val):
86 def sha1(val):
87 return hashlib.sha1(val).hexdigest()
87 return hashlib.sha1(val).hexdigest()
88
88
89
89
@@ -1,116 +1,116 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 """Extract the responses of a WSGI app."""
18 """Extract the responses of a WSGI app."""
19
19
20 __all__ = ('WSGIAppCaller',)
20 __all__ = ('WSGIAppCaller',)
21
21
22 import io
22 import io
23 import logging
23 import logging
24 import os
24 import os
25
25
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29 DEV_NULL = open(os.devnull)
29 DEV_NULL = open(os.devnull)
30
30
31
31
32 def _complete_environ(environ, input_data):
32 def _complete_environ(environ, input_data):
33 """Update the missing wsgi.* variables of a WSGI environment.
33 """Update the missing wsgi.* variables of a WSGI environment.
34
34
35 :param environ: WSGI environment to update
35 :param environ: WSGI environment to update
36 :type environ: dict
36 :type environ: dict
37 :param input_data: data to be read by the app
37 :param input_data: data to be read by the app
38 :type input_data: str
38 :type input_data: str
39 """
39 """
40 environ.update({
40 environ.update({
41 'wsgi.version': (1, 0),
41 'wsgi.version': (1, 0),
42 'wsgi.url_scheme': 'http',
42 'wsgi.url_scheme': 'http',
43 'wsgi.multithread': True,
43 'wsgi.multithread': True,
44 'wsgi.multiprocess': True,
44 'wsgi.multiprocess': True,
45 'wsgi.run_once': False,
45 'wsgi.run_once': False,
46 'wsgi.input': io.BytesIO(input_data),
46 'wsgi.input': io.BytesIO(input_data),
47 'wsgi.errors': DEV_NULL,
47 'wsgi.errors': DEV_NULL,
48 })
48 })
49
49
50
50
51 # pylint: disable=too-few-public-methods
51 # pylint: disable=too-few-public-methods
52 class _StartResponse(object):
52 class _StartResponse(object):
53 """Save the arguments of a start_response call."""
53 """Save the arguments of a start_response call."""
54
54
55 __slots__ = ['status', 'headers', 'content']
55 __slots__ = ['status', 'headers', 'content']
56
56
57 def __init__(self):
57 def __init__(self):
58 self.status = None
58 self.status = None
59 self.headers = None
59 self.headers = None
60 self.content = []
60 self.content = []
61
61
62 def __call__(self, status, headers, exc_info=None):
62 def __call__(self, status, headers, exc_info=None):
63 # TODO(skreft): do something meaningful with the exc_info
63 # TODO(skreft): do something meaningful with the exc_info
64 exc_info = None # avoid dangling circular reference
64 exc_info = None # avoid dangling circular reference
65 self.status = status
65 self.status = status
66 self.headers = headers
66 self.headers = headers
67
67
68 return self.write
68 return self.write
69
69
70 def write(self, content):
70 def write(self, content):
71 """Write method returning when calling this object.
71 """Write method returning when calling this object.
72
72
73 All the data written is then available in content.
73 All the data written is then available in content.
74 """
74 """
75 self.content.append(content)
75 self.content.append(content)
76
76
77
77
78 class WSGIAppCaller(object):
78 class WSGIAppCaller(object):
79 """Calls a WSGI app."""
79 """Calls a WSGI app."""
80
80
81 def __init__(self, app):
81 def __init__(self, app):
82 """
82 """
83 :param app: WSGI app to call
83 :param app: WSGI app to call
84 """
84 """
85 self.app = app
85 self.app = app
86
86
87 def handle(self, environ, input_data):
87 def handle(self, environ, input_data):
88 """Process a request with the WSGI app.
88 """Process a request with the WSGI app.
89
89
90 The returned data of the app is fully consumed into a list.
90 The returned data of the app is fully consumed into a list.
91
91
92 :param environ: WSGI environment to update
92 :param environ: WSGI environment to update
93 :type environ: dict
93 :type environ: dict
94 :param input_data: data to be read by the app
94 :param input_data: data to be read by the app
95 :type input_data: str
95 :type input_data: str
96
96
97 :returns: a tuple with the contents, status and headers
97 :returns: a tuple with the contents, status and headers
98 :rtype: (list<str>, str, list<(str, str)>)
98 :rtype: (list<str>, str, list<(str, str)>)
99 """
99 """
100 _complete_environ(environ, input_data)
100 _complete_environ(environ, input_data)
101 start_response = _StartResponse()
101 start_response = _StartResponse()
102 log.debug("Calling wrapped WSGI application")
102 log.debug("Calling wrapped WSGI application")
103 responses = self.app(environ, start_response)
103 responses = self.app(environ, start_response)
104 responses_list = list(responses)
104 responses_list = list(responses)
105 existing_responses = start_response.content
105 existing_responses = start_response.content
106 if existing_responses:
106 if existing_responses:
107 log.debug(
107 log.debug(
108 "Adding returned response to response written via write()")
108 "Adding returned response to response written via write()")
109 existing_responses.extend(responses_list)
109 existing_responses.extend(responses_list)
110 responses_list = existing_responses
110 responses_list = existing_responses
111 if hasattr(responses, 'close'):
111 if hasattr(responses, 'close'):
112 log.debug("Closing iterator from WSGI application")
112 log.debug("Closing iterator from WSGI application")
113 responses.close()
113 responses.close()
114
114
115 log.debug("Handling of WSGI request done, returning response")
115 log.debug("Handling of WSGI request done, returning response")
116 return responses_list, start_response.status, start_response.headers
116 return responses_list, start_response.status, start_response.headers
General Comments 0
You need to be logged in to leave comments. Login now