From patchwork Fri Oct 18 02:41:47 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1179095 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 46vVgf0m1kz9sPJ for ; Fri, 18 Oct 2019 13:42:38 +1100 (AEDT) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="Xlm2IDPE"; dkim-atps=neutral Received: from bilbo.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 46vVgd6y3dzDr0N for ; Fri, 18 Oct 2019 13:42:37 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::629; helo=mail-pl1-x629.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="Xlm2IDPE"; dkim-atps=neutral Received: from mail-pl1-x629.google.com (mail-pl1-x629.google.com [IPv6:2607:f8b0:4864:20::629]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 46vVg75vVGzDqt3 for ; Fri, 18 Oct 2019 13:42:11 +1100 (AEDT) Received: by mail-pl1-x629.google.com with SMTP id u12so2077437pls.12 for ; Thu, 17 Oct 2019 19:42:11 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=dCg16Kw72jdngQbg5o3JhgQ0O9GOk2rm15DqDxmSXTM=; b=Xlm2IDPEQ/SpX/sfAsEFxE2O1LJcU9DSj6MaPJrDL03V6eYJuIO6VE1ueLfzY3VPDy ZlWJzTam3wuv3t6ZsbGe7U3iIPKfEUmULzrVVoePH+TJslU6jhAn0SyVO2aDQqjhpuar msGRW9v5enu11xhQfVwFyRnNcu9lKlptuFbNY= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=dCg16Kw72jdngQbg5o3JhgQ0O9GOk2rm15DqDxmSXTM=; b=K4k+auzqS+Bo2I5odokNwceC6E678NJqm18XFTtcks6PuiOCZ68CwWVuS0A0B/5yRe 1Kg/hEQP4rfqboChG2Lyy1E9+Bhm26qPE7mzgmxkYkRS7tL6uVXpIaPuE+NKSnm2hlmH QYwJ82VVo9XV7yigImsN4HPTgbhxF9iEyQB4Rcbnr1znidkdjnkKESvL/lQOHZnTKGR1 e3LrHl19k1tSam4EDUK9MNivb1Yjd/1M6F06pC/XxPHYMgOo0aF70xT8/OuiJG5UdU5E 45M4FQfQY1jXrA/e5EC3MZYoeT2C22M/cPFtoqbgjvpsIXos+PZ/lUwg203smoXJaSNK JKVQ== X-Gm-Message-State: APjAAAW8FguvTPRIlDdMxEMumTL2vxO1chuRvOD8huGmtqPQyKbUB+nN 7nXlCYLnxsva8Idvz3dF6dpS/VK0kyY= X-Google-Smtp-Source: APXvYqy4Rd/QYoHA9JfGpqLwR2fMS0Kyp60nSBmQQXGVNupME7Q0p8vZx5nQ0sNBN0VmP0EE6t6IEw== X-Received: by 2002:a17:902:788d:: with SMTP id q13mr7607179pll.41.1571366528949; Thu, 17 Oct 2019 19:42:08 -0700 (PDT) Received: from localhost (ppp167-251-205.static.internode.on.net. [59.167.251.205]) by smtp.gmail.com with ESMTPSA id v6sm3239369pgv.24.2019.10.17.19.42.07 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Oct 2019 19:42:08 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 1/3] docs: document snowpatch as an API client Date: Fri, 18 Oct 2019 13:41:47 +1100 Message-Id: <20191018024149.8775-2-dja@axtens.net> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20191018024149.8775-1-dja@axtens.net> References: <20191018024149.8775-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Snowpatch is one of the few publically visible API clients. Document it in the clients list. Signed-off-by: Daniel Axtens Reviewed-by: Stephen Finucane --- docs/usage/clients.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git docs/usage/clients.rst docs/usage/clients.rst index 57c8a1a14a4d..01dd62a28e50 100644 --- docs/usage/clients.rst +++ docs/usage/clients.rst @@ -40,3 +40,16 @@ instructions, can be found in the `documentation`__ and the `GitHub repo`__. __ https://git-pw.readthedocs.io/ __ https://github.com/getpatchwork/git-pw/ + + +snowpatch +--------- + +The :program:`snowpatch` application is a bridge between Patchwork and the +Jenkins continuous integration automation server. It monitors the REST API +for incoming patches, applies them on top of an existing git tree, triggers +appropriate builds and test suites, and reports the results back to Patchwork. + +Find out more about :program:`snowpatch` at its `GitHub repo`__. + +__ https://github.com/ruscur/snowpatch From patchwork Fri Oct 18 02:41:48 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1179096 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [203.11.71.2]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 46vVgz1J5kz9sPJ for ; Fri, 18 Oct 2019 13:42:55 +1100 (AEDT) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="kdFI9Kn6"; dkim-atps=neutral Received: from bilbo.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 46vVgy72TzzDr0v for ; Fri, 18 Oct 2019 13:42:54 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::535; helo=mail-pg1-x535.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="kdFI9Kn6"; dkim-atps=neutral Received: from mail-pg1-x535.google.com (mail-pg1-x535.google.com [IPv6:2607:f8b0:4864:20::535]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 46vVgC738PzDqt3 for ; Fri, 18 Oct 2019 13:42:15 +1100 (AEDT) Received: by mail-pg1-x535.google.com with SMTP id f14so2463107pgi.9 for ; Thu, 17 Oct 2019 19:42:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=33cn2aWGHLaJqT3BbSpY2gMW22BCiJi3vTWAb9UcqMA=; b=kdFI9Kn6klN69S61LbCs/fJx7WQ9/pcsNJQlpV7PFf4ujLwcXuNM47MaWunYHMuyF3 1r6Fs8eLScQyg+ZfKL8uelANautOot31L7dRX4tCOKc4vJxmkjttuc8W79wm5CPp123X mvKvmxH7LnT2FpRuWEQ5YROqQGtjfog4408VY= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=33cn2aWGHLaJqT3BbSpY2gMW22BCiJi3vTWAb9UcqMA=; b=FFEkekmY+a4ND05z8QdelrTM/3UOtICPoayWPhPw3Z6ZOh6dfRWQA+zWhuFlSmMPdz fbUf2PzpHTCWxJAq97Ev7wahxjsP7bbTm3aWvlu7lKel4XLe/VHHs+j+6pzaxhQkfXCz CwNSRtkF6DRv4+EPvstgkv8+Rv2K2FcctDC+5fcvuqZhvYrriKSDi056yFUcgiLcNnCT n/KBqixXF4FS3t4gte4sFeJ17mSI7ocuoCp0EGfVFx6ZRBdo/c39gWf784rTRoHbQvIu 9Lq0fHsFxPW9Bv3jgyPdsL8dGJHkC8d36rCz9GcUxJcAwghdeeFpyc845OvhlqQcrafD 03xA== X-Gm-Message-State: APjAAAWMVLBvX46k5Iyl1q01Bri+GIqZAp+shcvjIUIzHYG6tJf+drBu GAT2m0fLDD5LwW+cCcg3WeGhqCmmHqg= X-Google-Smtp-Source: APXvYqyDOmDbqyq/HnbUVBIigsLr0dlboUYvh9r3vnXrgbgJLs5clCMirNXWGpRE9NQy16e+5zUhww== X-Received: by 2002:a63:6301:: with SMTP id x1mr7927131pgb.271.1571366533179; Thu, 17 Oct 2019 19:42:13 -0700 (PDT) Received: from localhost (ppp167-251-205.static.internode.on.net. [59.167.251.205]) by smtp.gmail.com with ESMTPSA id k23sm3850180pgi.49.2019.10.17.19.42.11 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Oct 2019 19:42:12 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 2/3] docs: bump the copyright year in the docs Date: Fri, 18 Oct 2019 13:41:48 +1100 Message-Id: <20191018024149.8775-3-dja@axtens.net> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20191018024149.8775-1-dja@axtens.net> References: <20191018024149.8775-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" It's 2019. It's almost 2020, in fact! Signed-off-by: Daniel Axtens Reviewed-by: Stephen Finucane --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git docs/conf.py docs/conf.py index f0ca797f6dac..646db889e731 100644 --- docs/conf.py +++ docs/conf.py @@ -34,7 +34,7 @@ master_doc = 'index' # General information about the project. project = u'Patchwork' -copyright = u'2018, Patchwork Developers' +copyright = u'2018-2019, Patchwork Developers' author = u'Patchwork Developers' # The version info for the project you're documenting, acts as replacement for From patchwork Fri Oct 18 02:41:49 2019 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1179097 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 46vVhH71Spz9sPJ for ; Fri, 18 Oct 2019 13:43:11 +1100 (AEDT) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="Al6URGvI"; dkim-atps=neutral Received: from bilbo.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 46vVhH6C8bzDr13 for ; Fri, 18 Oct 2019 13:43:11 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::532; helo=mail-pg1-x532.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=none (p=none dis=none) header.from=axtens.net Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.b="Al6URGvI"; dkim-atps=neutral Received: from mail-pg1-x532.google.com (mail-pg1-x532.google.com [IPv6:2607:f8b0:4864:20::532]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 46vVgN1kfkzDr2j for ; Fri, 18 Oct 2019 13:42:24 +1100 (AEDT) Received: by mail-pg1-x532.google.com with SMTP id 23so2483480pgk.3 for ; Thu, 17 Oct 2019 19:42:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=sxCX5+tcJzXR6yzZn0OHvzTdydUbTm4kj8Epn8bPoOU=; b=Al6URGvIQUWYUioDrmSj9X63t6bXVK52/yS+uLPG1ypEUnXaY2Di3+dFOoCN27PBiz iJGY09kpEnBq0zoq7FmjrJqLhMST9lFzGgm/oohceDLyjj7eXNaXtKsQEW6sCp3uoE1N iMg8ZgYe7Az5y10xf9afb/fPSujZCuZhZGpGU= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=sxCX5+tcJzXR6yzZn0OHvzTdydUbTm4kj8Epn8bPoOU=; b=MRnI2IW+LFfbMbJKGsoKEbWBU52psaDgjdizDd7WxUwRzphxZsbs0MvRnxwnGCbMOi Ko1lr2qnJqTpw8PNSR81Cltl96xQ3sPnTapU5NAm8qB9GlxXF+24hdkY0MU2wat6VAam R+ytSqya5ZZess7sfNxIyFpxdINdlpjF3snvqyZjm5nFPFwyygVuyYvUO208Z1n5c0C4 1Vddgrf1i26hQSMFsYwely7lpKFgNXFrLnA5eyS4bN3LOEbv0PsHFePStwP2cf5WecO4 4AwTat81aDlj2wp8e8QXO8h/XP68zePKCTvXywKiRLcAIuKlwnIVFWFE7HgFjTUAv71k 5fKA== X-Gm-Message-State: APjAAAXI1gN25Fv2JXpK2ETMt4PXgO36jIDb+DP0pE3yOqJxlhAI2K7N y+i9q0c5xN1T9T9NM5gBgvTh/w6lX38= X-Google-Smtp-Source: APXvYqwUB1wK7RWy2LLmbPieF/5qmw85u5Kp3SXJFOhym7QRv60t7sec5oMmO4Nlf3GzAtfnML845Q== X-Received: by 2002:a63:7356:: with SMTP id d22mr7471624pgn.230.1571366539032; Thu, 17 Oct 2019 19:42:19 -0700 (PDT) Received: from localhost (ppp167-251-205.static.internode.on.net. [59.167.251.205]) by smtp.gmail.com with ESMTPSA id h4sm4202876pfg.159.2019.10.17.19.42.16 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 17 Oct 2019 19:42:17 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 3/3] [PW3] Remove XML-RPC API Date: Fri, 18 Oct 2019 13:41:49 +1100 Message-Id: <20191018024149.8775-4-dja@axtens.net> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20191018024149.8775-1-dja@axtens.net> References: <20191018024149.8775-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" It's been deprecated since 2.0 with the new REST API. That API is now pretty solid, and git-pw is good. Drop the old API. Provide a page letting people know that the API is gone if they access any of the old pages. This breaks pwclient, which only supports the old API. So we delete a few things that referred to it or used it, including some old tools. Signed-off-by: Daniel Axtens --- docs/TODO | 6 - docs/api/index.rst | 5 +- docs/api/rest/index.rst | 4 +- docs/api/xmlrpc.rst | 64 -- docs/deployment/configuration.rst | 4 - docs/deployment/installation.rst | 35 +- docs/deployment/management.rst | 6 +- docs/development/api.rst | 5 +- docs/index.rst | 1 - docs/usage/clients.rst | 27 +- patchwork/settings/base.py | 3 - patchwork/settings/dev.py | 2 - patchwork/templates/patchwork/about.html | 10 - patchwork/templates/patchwork/project.html | 16 +- patchwork/templates/patchwork/pwclientrc | 15 - .../templates/patchwork/xmlrpc-removed.html | 16 + patchwork/tests/test_about.py | 16 - patchwork/tests/test_xmlrpc.py | 224 ----- patchwork/urls.py | 20 +- patchwork/views/about.py | 6 +- patchwork/views/project.py | 2 - patchwork/views/pwclient.py | 28 - patchwork/views/removed.py | 11 + patchwork/views/xmlrpc.py | 951 ------------------ .../notes/remove-xmlrpc-b6d26084338efcb4.yaml | 11 + tools/patchwork-update-commits | 20 - tools/post-receive.hook | 86 -- 27 files changed, 61 insertions(+), 1533 deletions(-) delete mode 100644 docs/api/xmlrpc.rst delete mode 100644 patchwork/templates/patchwork/pwclientrc create mode 100644 patchwork/templates/patchwork/xmlrpc-removed.html delete mode 100644 patchwork/tests/test_xmlrpc.py delete mode 100644 patchwork/views/pwclient.py create mode 100644 patchwork/views/removed.py delete mode 100644 patchwork/views/xmlrpc.py create mode 100644 releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml delete mode 100755 tools/patchwork-update-commits delete mode 100755 tools/post-receive.hook diff --git docs/TODO docs/TODO index 37c30fd951dd..693e4bb743be 100644 --- docs/TODO +++ docs/TODO @@ -7,10 +7,4 @@ * store rejected mails * In-message From: header * Per-user default filter settings -* pwclient: -b to specify multiple patch ids -* pwclient: add bundle manipulation and retrieval ops -* pwclient: specify multiple patches by ID range -* pwclient: integrate hash parser -* pwclient: add example scripts (eg, git post-commit hook) to online help -* pwclient: case-insensitive searches for author and project name * changing primary email addresses for accounts diff --git docs/api/index.rst docs/api/index.rst index c679dae133bf..6e89b7197e39 100644 --- docs/api/index.rst +++ docs/api/index.rst @@ -3,12 +3,9 @@ API Documentation ================= -Patchwork provides two APIs: a REST API and a legacy XML-RPC API. The REST API -is recommended as the XML-RPC API is deprecated and will be removed in a future -release. +Patchwork provides a REST API. .. toctree:: :maxdepth: 2 /api/rest/index - /api/xmlrpc diff --git docs/api/rest/index.rst docs/api/rest/index.rst index d1169e56e07a..293490b2874f 100644 --- docs/api/rest/index.rst +++ docs/api/rest/index.rst @@ -25,8 +25,8 @@ If all you want is reference guides, skip straight to :ref:`rest-api-schemas`. .. versionadded:: 2.0 The REST API was introduced in Patchwork v2.0. Users of earlier Patchwork - versions should instead refer to :doc:`XML-RPC API ` - documentation. + versions should instead refer to XML-RPC API documentation provided with + those versions. .. versionchanged:: 2.1 diff --git docs/api/xmlrpc.rst docs/api/xmlrpc.rst deleted file mode 100644 index 5412cce5d155..000000000000 --- docs/api/xmlrpc.rst +++ /dev/null @@ -1,64 +0,0 @@ -The XML-RPC API -=============== - -Patchwork provides an XML-RPC API. This API can be used to be used to retrieve -and modify information about patches, projects and more. - -.. important:: - - The XML-RPC API can be enabled/disabled by the administrator: it may not be - available in every instance. Refer to ``/about`` on your given instance for - the status of the API, e.g. - - https://patchwork.ozlabs.org/about - - Alternatively, simply attempt to make a request to the API. - -.. deprecated:: 2.0 - - The XML-RPC API is a legacy API and has been deprecated in favour of the - :doc:`REST API `. It will be removed in Patchwork 3.0. - -Getting Started ---------------- - -The Patchwork XML-RPC API provides a number of "methods". Some methods require -authentication (via HTTP Basic Auth) while others do not. Authentication uses -your Patchwork account and the on-server documentation will indicate where it -is necessary. We will only cover the unauthenticated method here for brevity - -consult the `xmlrpclib`_ documentation for more detailed examples: - -To interact with the Patchwork XML-RPC API, a XML-RPC library should be used. -Python provides such a library - `xmlrpclib`_ - in its standard library. For -example, to get the version of the XML-RPC API for a Patchwork instance hosted -at `patchwork.example.com`, run: - -.. code-block:: pycon - - $ python - >>> import xmlrpclib # or 'xmlrpc.client' for Python 3 - >>> rpc = xmlrpclib.ServerProxy('http://patchwork.example.com/xmlrpc/') - >>> rpc.pw_rpc_version() - 1.1 - -Once connected, the ``rpc`` object will be populated with a list of available -functions (or procedures, in RPC terminology). In the above example, we used -the ``pw_rpc_version`` method, however, it should be possible to use all the -methods listed in the server documentation. - -Further Information -------------------- - -Patchwork provides automatically generated documentation for the XML-RPC API. -You can find this at the following URL: - - https://patchwork.example.com/xmlrpc/ - -where `patchwork.example.com` refers to the URL of your Patchwork instance. - -.. versionchanged:: 1.1 - - Automatic documentation generation for the Patchwork API was introduced in - Patchwork v1.1. Prior versions of Patchwork do not offer this functionality. - -.. _xmlrpclib: https://docs.python.org/2/library/xmlrpclib.html diff --git docs/deployment/configuration.rst docs/deployment/configuration.rst index a71dd3f4bb57..482712f96773 100644 --- docs/deployment/configuration.rst +++ docs/deployment/configuration.rst @@ -80,10 +80,6 @@ Enable the :doc:`REST API <../api/rest/index>`. .. versionadded:: 2.0 -``ENABLE_XMLRPC`` -~~~~~~~~~~~~~~~~~ - -Enable the :doc:`XML-RPC API <../api/xmlrpc>`. .. TODO(stephenfin) Deprecate this in favor of SECURE_SSL_REDIRECT diff --git docs/deployment/installation.rst docs/deployment/installation.rst index f477a110f292..a505796e6381 100644 --- docs/deployment/installation.rst +++ docs/deployment/installation.rst @@ -280,15 +280,8 @@ described in :doc:`configuration`. * ``NOTIFICATION_FROM_EMAIL`` These are not configurable using environment variables and must be configured -directly in the ``production.py`` settings file instead. For example, if you -wish to enable the XML-RPC API, you should add the following: - -.. code-block:: python - - ENABLE_XMLRPC = True - -Similarly, should you wish to disable the REST API, you should add the -following: +directly in the ``production.py`` settings file instead. For example, should +you wish to disable the REST API, you should add the following: .. code-block:: python @@ -506,8 +499,7 @@ doing the following: Once the administrative console is accessible, you would want to configure your different sites and their corresponding domain names, which is required for the -different emails sent by Patchwork (registration, password recovery) as well as -the sample ``pwclientrc`` files provided by your project's page. +different emails sent by Patchwork (registration, password recovery). .. _deployment-parsemail: @@ -646,27 +638,6 @@ You can also create such as service yourself using a PaaS provider that supports incoming mail and writing a little web app. -.. _deployment-vcs: - -(Optional) Configure your VCS to Automatically Update Patches -------------------------------------------------------------- - -The ``tools`` directory of the Patchwork distribution contains a file named -``post-receive.hook`` which is a sample Git hook that can be used to -automatically update patches to the *Accepted* state when corresponding commits -are pushed via Git. - -To install this hook, simply copy it to the ``.git/hooks`` directory on your -server, name it ``post-receive``, and make it executable. - -This sample hook has support to update patches to different states depending on -which branch is being pushed to. See the ``STATE_MAP`` setting in that file. - -If you are using a system other than Git, you can likely write a similar hook -using the :doc:`APIs ` or :doc:`API clients ` to to -update patch state. If you do write one, please contribute it. - - .. _deployment-cron: (Optional) Configure the Patchwork Cron Job diff --git docs/deployment/management.rst docs/deployment/management.rst index 9c57f1962283..0881dc296cc9 100644 --- docs/deployment/management.rst +++ docs/deployment/management.rst @@ -128,9 +128,9 @@ Update the hashes on existing patches. ./manage.py rehash [, ...] Patchwork stores hashes for each patch it receives. These hashes can be used to -uniquely identify a patch for things like :ref:`automatically changing the -state of the patch in Patchwork when it merges `. If you change -your hashing algorithm, you may wish to rehash the patches. +uniquely identify a patch for things like automatically changing the state of +the patch in Patchwork when it is merged. If you change your hashing algorithm, +you may wish to rehash the patches. .. option:: patch_id diff --git docs/development/api.rst docs/development/api.rst index cea7bc78dd55..e5298e5e9d23 100644 --- docs/development/api.rst +++ docs/development/api.rst @@ -3,9 +3,8 @@ Using the APIs ============== -Patchwork provides two APIs: the legacy :doc:`XML-RPC API ` and -the :doc:`REST API `. You can use these APIs to interact with -Patchwork programmatically and to develop your own clients. +Patchwork provides a :doc:`REST API `. You can use this API +to interact with Patchwork programmatically and to develop your own clients. For quick usage examples of the APIs, refer to the :doc:`documentation <../api/index>`. For examples of existing clients, refer to diff --git docs/index.rst docs/index.rst index b73c647c5222..1eff93d83080 100644 --- docs/index.rst +++ docs/index.rst @@ -51,7 +51,6 @@ of community projects. :caption: API Documentation api/rest/index - api/xmlrpc .. toctree:: :maxdepth: 2 diff --git docs/usage/clients.rst docs/usage/clients.rst index 01dd62a28e50..40b93f27b344 100644 --- docs/usage/clients.rst +++ docs/usage/clients.rst @@ -1,31 +1,8 @@ Clients ======= -A number of clients are available for interacting with Patchwork's various -APIs. - - -pwclient --------- - -.. versionchanged:: 2.2 - - :program:`pwclient` was previously provided with Patchwork. It has been - packaged as a separate application since Patchwork v2.2.0. - -The :program:`pwclient` application can be used to interact with Patchwork from -the command line. Functionality provided by :program:`pwclient` includes: - -- Listing patches, projects, and checks -- Downloading and applying patches to a local code base -- Modifying the status of patches -- Creating new checks - -More information on :program:`pwclient`, including installation and usage -instructions, can be found in the `documentation`__ and the `GitHub repo`__. - -__ https://pwclient.readthedocs.io/ -__ https://github.com/getpatchwork/pwclient/ +A REST client is available for interacting with Patchwork's API, and other +projects build on top of the API to provide other functionality. git-pw diff --git patchwork/settings/base.py patchwork/settings/base.py index b86cdc276d5a..fbb0b0d8565b 100644 --- patchwork/settings/base.py +++ patchwork/settings/base.py @@ -211,9 +211,6 @@ NOTIFICATION_DELAY_MINUTES = 10 NOTIFICATION_FROM_EMAIL = DEFAULT_FROM_EMAIL -# Set to True to enable the Patchwork XML-RPC interface -ENABLE_XMLRPC = False - # Set to True to enable the Patchwork REST API ENABLE_REST_API = True diff --git patchwork/settings/dev.py patchwork/settings/dev.py index e110e74579ca..141915fdd001 100644 --- patchwork/settings/dev.py +++ patchwork/settings/dev.py @@ -99,6 +99,4 @@ if dbbackup: # Patchwork settings # -ENABLE_XMLRPC = True - ENABLE_REST_API = True diff --git patchwork/templates/patchwork/about.html patchwork/templates/patchwork/about.html index 210e9513c4f4..9c83ba0e4173 100644 --- patchwork/templates/patchwork/about.html +++ patchwork/templates/patchwork/about.html @@ -56,16 +56,6 @@ disabled {% endif %} -
  • - XML-RPC - - {% if enabled_apis.xmlrpc %} - enabled - {% else %} - disabled - {% endif %} -
  • diff --git patchwork/templates/patchwork/project.html patchwork/templates/patchwork/project.html index bd9d20e263d8..ce647b9df04b 100644 --- patchwork/templates/patchwork/project.html +++ patchwork/templates/patchwork/project.html @@ -55,19 +55,5 @@ {% endif %} -{% if enable_xmlrpc %} -

    pwclient

    - -

    pwclient is the command-line client for Patchwork. Currently, -it provides access to some read-only features of Patchwork, such as downloading -and applying patches.

    - -

    To use pwclient, you will need:

    -
      -
    • The pwclient program
    • -
    • (optional) A .pwclientrc file for this project, which should be stored in your - home directory.
    • -
    -{% endif %} + {% endblock %} diff --git patchwork/templates/patchwork/pwclientrc patchwork/templates/patchwork/pwclientrc deleted file mode 100644 index 7d466d890da5..000000000000 --- patchwork/templates/patchwork/pwclientrc +++ /dev/null @@ -1,15 +0,0 @@ -# Sample .pwclientrc file for the {{ project.linkname }} project, -# running on {{ site.domain }}. -# -# Just append this file to your existing ~/.pwclientrc -# If you do not already have a ~/.pwclientrc, then copy this file to -# ~/.pwclientrc, and uncomment the following two lines: -# [options] -# default={{ project.linkname }} - -[{{ project.linkname }}] -url = {{ scheme }}://{{ site.domain }}{% url 'xmlrpc' %} -{% if user.is_authenticated %} -username = {{ user.username }} -password = -{% endif %} diff --git patchwork/templates/patchwork/xmlrpc-removed.html patchwork/templates/patchwork/xmlrpc-removed.html new file mode 100644 index 000000000000..bc2e41ebf313 --- /dev/null +++ patchwork/templates/patchwork/xmlrpc-removed.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}XML-RPC API removed{% endblock %} +{% block heading %}XML-RPC API removed{% endblock %} + +{% block body %} +
    +

    XML-RPC API removed

    + +

    The XML-RPC API has been removed in Patchwork 3 in favour of the REST API.

    + +

    If you were using pwclient, try + git-pw. +

    +
    +{% endblock %} diff --git patchwork/tests/test_about.py patchwork/tests/test_about.py index d8c35b9f3e5f..2d9385d8ea8b 100644 --- patchwork/tests/test_about.py +++ patchwork/tests/test_about.py @@ -15,7 +15,6 @@ class AboutViewTest(TestCase): def _test_redirect(self, view): requested_url = reverse(view) redirect_url = reverse('about') - response = self.client.get(requested_url) self.assertRedirects(response, redirect_url, 301) @@ -23,21 +22,6 @@ class AboutViewTest(TestCase): for view in ['help', 'help-about']: self._test_redirect(view) - @unittest.skipUnless(settings.ENABLE_XMLRPC, - 'requires xmlrpc interface (use the ENABLE_XMLRPC ' - 'setting)') - def test_redirects_xmlrpc(self): - self._test_redirect('help-pwclient') - - def test_xmlrpc(self): - with self.settings(ENABLE_XMLRPC=False): - response = self.client.get(reverse('about')) - self.assertFalse(response.context['enabled_apis']['xmlrpc']) - - with self.settings(ENABLE_XMLRPC=True): - response = self.client.get(reverse('about')) - self.assertTrue(response.context['enabled_apis']['xmlrpc']) - def test_rest(self): with self.settings(ENABLE_REST_API=False): response = self.client.get(reverse('about')) diff --git patchwork/tests/test_xmlrpc.py patchwork/tests/test_xmlrpc.py deleted file mode 100644 index 79c6c848a0c2..000000000000 --- patchwork/tests/test_xmlrpc.py +++ /dev/null @@ -1,224 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2014 Jeremy Kerr -# -# SPDX-License-Identifier: GPL-2.0-or-later - -import unittest - -from django.conf import settings -from django.test import LiveServerTestCase -from django.urls import reverse -from django.utils.six.moves import xmlrpc_client - -from patchwork.tests import utils - - -@unittest.skipUnless(settings.ENABLE_XMLRPC, - 'requires xmlrpc interface (use the ENABLE_XMLRPC ' - 'setting)') -class XMLRPCTest(LiveServerTestCase): - - def setUp(self): - self.url = self.live_server_url + reverse('xmlrpc') - self.rpc = xmlrpc_client.Server(self.url) - - -class XMLRPCGenericTest(XMLRPCTest): - - def test_pw_rpc_version(self): - # If you update the RPC version, update the tests! - self.assertEqual(self.rpc.pw_rpc_version(), [1, 3, 0]) - - def test_get_redirect(self): - response = self.client.patch(self.url) - self.assertRedirects(response, reverse('project-list')) - - def test_invalid_method(self): - with self.assertRaises(xmlrpc_client.Fault): - self.rpc.xyzzy() - - def test_absent_auth(self): - with self.assertRaises(xmlrpc_client.Fault): - self.rpc.patch_set(0, {}) - - -@unittest.skipUnless(settings.ENABLE_XMLRPC, - 'requires xmlrpc interface (use the ENABLE_XMLRPC ' - 'setting)') -class XMLRPCAuthenticatedTest(LiveServerTestCase): - - def setUp(self): - self.url = self.live_server_url + reverse('xmlrpc') - # url is of the form http://localhost:PORT/PATH - # strip the http and replace it with the username/passwd of a user. - self.project = utils.create_project() - self.user = utils.create_maintainer(self.project) - self.url = ('http://%s:%s@' + self.url[7:]) % (self.user.username, - self.user.username) - self.rpc = xmlrpc_client.Server(self.url) - - def test_patch_set(self): - patch = utils.create_patch(project=self.project) - result = self.rpc.patch_get(patch.id) - self.assertFalse(result['archived']) - - self.rpc.patch_set(patch.id, {'archived': True}) - - # reload the patch - result = self.rpc.patch_get(patch.id) - self.assertTrue(result['archived']) - - -class XMLRPCModelTestMixin(object): - - def create_multiple(self, count): - return [self.create_single() for i in range(count)] - - def test_get_none(self): - self.assertEqual(self.get_endpoint(0), {}) - - def test_list_none(self): - self.assertEqual(self.list_endpoint(), []) - - def test_list_single(self): - obj = self.create_single() - result = self.list_endpoint() - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], obj.id) - - def test_list_named(self): - obj = self.create_single(name='FOOBARBAZ') - self.create_multiple(5) - result = self.list_endpoint('oobarb') - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], obj.id) - - def test_list_named_none(self): - self.create_multiple(5) - result = self.list_endpoint('invisible') - self.assertEqual(len(result), 0) - - def test_get_single(self): - obj = self.create_single() - result = self.get_endpoint(obj.id) - self.assertEqual(result['id'], obj.id) - - def test_get_invalid(self): - obj = self.create_single() - result = self.get_endpoint(obj.id + 1) - self.assertEqual(result, {}) - - def test_list_multiple(self): - self.create_multiple(5) - result = self.list_endpoint() - self.assertEqual(len(result), 5) - - def test_list_max_count(self): - objs = self.create_multiple(5) - result = self.list_endpoint("", 2) - self.assertEqual(len(result), 2) - self.assertEqual(result[0]['id'], objs[0].id) - - def test_list_negative_max_count(self): - objs = self.create_multiple(5) - result = self.list_endpoint("", -1) - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], objs[-1].id) - - -class XMLRPCFilterModelTestMixin(XMLRPCModelTestMixin): - - # override these tests due to the way you pass in filters - def test_list_max_count(self): - objs = self.create_multiple(5) - result = self.list_endpoint({'max_count': 2}) - self.assertEqual(len(result), 2) - self.assertEqual(result[0]['id'], objs[0].id) - - def test_list_negative_max_count(self): - objs = self.create_multiple(5) - result = self.list_endpoint({'max_count': -1}) - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], objs[-1].id) - - def test_list_named(self): - obj = self.create_single(name='FOOBARBAZ') - self.create_multiple(5) - result = self.list_endpoint({'name__icontains': 'oobarb'}) - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], obj.id) - - def test_list_named_none(self): - self.create_multiple(5) - result = self.list_endpoint({'name__icontains': 'invisible'}) - self.assertEqual(len(result), 0) - - -class XMLRPCPatchTest(XMLRPCTest, XMLRPCFilterModelTestMixin): - def setUp(self): - super(XMLRPCPatchTest, self).setUp() - self.get_endpoint = self.rpc.patch_get - self.list_endpoint = self.rpc.patch_list - self.create_multiple = utils.create_patches - - def create_single(self, **kwargs): - return utils.create_patches(**kwargs)[0] - - def test_patch_check_get(self): - patch = self.create_single() - check = utils.create_check(patch=patch) - result = self.rpc.patch_check_get(patch.id) - self.assertEqual(result['total'], 1) - self.assertEqual(result['checks'][0]['id'], check.id) - self.assertEqual(result['checks'][0]['patch_id'], patch.id) - - def test_patch_get_by_hash(self): - patch = self.create_single() - result = self.rpc.patch_get_by_hash(patch.hash) - self.assertEqual(result['id'], patch.id) - - -class XMLRPCPersonTest(XMLRPCTest, XMLRPCModelTestMixin): - - def setUp(self): - super(XMLRPCPersonTest, self).setUp() - self.get_endpoint = self.rpc.person_get - self.list_endpoint = self.rpc.person_list - self.create_single = utils.create_person - - -class XMLRPCProjectTest(XMLRPCTest, XMLRPCModelTestMixin): - - def setUp(self): - super(XMLRPCProjectTest, self).setUp() - self.get_endpoint = self.rpc.project_get - self.list_endpoint = self.rpc.project_list - self.create_single = utils.create_project - - def test_list_named(self): - # project filters by linkname, not name! - obj = self.create_single(linkname='FOOBARBAZ') - result = self.list_endpoint('oobarb') - self.assertEqual(len(result), 1) - self.assertEqual(result[0]['id'], obj.id) - - -class XMLRPCStateTest(XMLRPCTest, XMLRPCModelTestMixin): - - def setUp(self): - super(XMLRPCStateTest, self).setUp() - self.get_endpoint = self.rpc.state_get - self.list_endpoint = self.rpc.state_list - self.create_single = utils.create_state - - -class XMLRPCCheckTest(XMLRPCTest, XMLRPCFilterModelTestMixin): - - def setUp(self): - super(XMLRPCCheckTest, self).setUp() - self.get_endpoint = self.rpc.check_get - self.list_endpoint = self.rpc.check_list - self.create_single = utils.create_check - - def test_list_named(self): - pass diff --git patchwork/urls.py patchwork/urls.py index dcdcfb49e67e..2f2b448da331 100644 --- patchwork/urls.py +++ patchwork/urls.py @@ -18,10 +18,9 @@ from patchwork.views import mail as mail_views from patchwork.views import notification as notification_views from patchwork.views import patch as patch_views from patchwork.views import project as project_views -from patchwork.views import pwclient as pwclient_views from patchwork.views import series as series_views from patchwork.views import user as user_views -from patchwork.views import xmlrpc as xmlrpc_views +from patchwork.views import removed as removed_views admin.autodiscover() @@ -163,16 +162,6 @@ if 'debug_toolbar' in settings.INSTALLED_APPS: url(r'^__debug__/', include(debug_toolbar.urls)), ] -if settings.ENABLE_XMLRPC: - urlpatterns += [ - url(r'xmlrpc/$', xmlrpc_views.xmlrpc, name='xmlrpc'), - url(r'^project/(?P[^/]+)/pwclientrc/$', - pwclient_views.pwclientrc, - name='pwclientrc'), - # legacy redirect - url(r'^help/pwclient/$', about_views.redirect, name='help-pwclient'), - ] - if settings.ENABLE_REST_API: if 'rest_framework' not in settings.INSTALLED_APPS: raise RuntimeError( @@ -276,3 +265,10 @@ if settings.COMPAT_REDIR: bundle_views.bundle_mbox_redir, name='bundle-mbox-redir'), ] + + urlpatterns += [ + url(r'xmlrpc/$', removed_views.xmlrpc_removed), + url(r'^project/(?P[^/]+)/pwclientrc/$', + removed_views.xmlrpc_removed), + url(r'^help/pwclient/$', removed_views.xmlrpc_removed), + ] diff --git patchwork/views/about.py patchwork/views/about.py index 91c3b74ebf8f..0a6f75d3b218 100644 --- patchwork/views/about.py +++ patchwork/views/about.py @@ -14,7 +14,6 @@ def about(request): context = { 'enabled_apis': { 'rest': settings.ENABLE_REST_API, - 'xmlrpc': settings.ENABLE_XMLRPC, }, 'admins': () if settings.ADMINS_HIDE else settings.ADMINS, } @@ -23,8 +22,5 @@ def about(request): def redirect(request): - """Redirect for legacy URLs. - - Remove this when Patchwork 3.0 is released. - """ + """Redirect for legacy URLs.""" return HttpResponsePermanentRedirect(reverse('about')) diff --git patchwork/views/project.py patchwork/views/project.py index 8fa41794f5db..621457d5b732 100644 --- patchwork/views/project.py +++ patchwork/views/project.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -from django.conf import settings from django.contrib.auth.models import User from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -59,6 +58,5 @@ def project_detail(request, project_id): profile__maintainer_projects=project).select_related('profile'), 'n_patches': n_patches[False] if False in n_patches else 0, 'n_archived_patches': n_patches[True] if True in n_patches else 0, - 'enable_xmlrpc': settings.ENABLE_XMLRPC, } return render(request, 'patchwork/project.html', context) diff --git patchwork/views/pwclient.py patchwork/views/pwclient.py deleted file mode 100644 index 72ebcbbb9e90..000000000000 --- patchwork/views/pwclient.py +++ /dev/null @@ -1,28 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2008 Jeremy Kerr -# -# SPDX-License-Identifier: GPL-2.0-or-later - -from django.conf import settings -from django.shortcuts import get_object_or_404 -from django.shortcuts import render - -from patchwork.models import Project - - -def pwclientrc(request, project_id): - project = get_object_or_404(Project, linkname=project_id) - - context = { - 'project': project, - } - if settings.FORCE_HTTPS_LINKS or request.is_secure(): - context['scheme'] = 'https' - else: - context['scheme'] = 'http' - - response = render(request, 'patchwork/pwclientrc', context, - content_type='text/plain') - response['Content-Disposition'] = 'attachment; filename=.pwclientrc' - - return response diff --git patchwork/views/removed.py patchwork/views/removed.py new file mode 100644 index 000000000000..7db3c0e3b8d0 --- /dev/null +++ patchwork/views/removed.py @@ -0,0 +1,11 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2019 IBM Corporation +# Author: Daniel Axtens +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from django.shortcuts import render + + +def xmlrpc_removed(request, project_id=None): + return render(request, 'patchwork/xmlrpc-removed.html', {}, status=410) diff --git patchwork/views/xmlrpc.py patchwork/views/xmlrpc.py deleted file mode 100644 index f60725044ebe..000000000000 --- patchwork/views/xmlrpc.py +++ /dev/null @@ -1,951 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2008 Jeremy Kerr -# -# SPDX-License-Identifier: GPL-2.0-or-later - -import base64 -# NOTE(stephenfin) six does not seem to support this -try: - from DocXMLRPCServer import XMLRPCDocGenerator -except ImportError: - from xmlrpc.server import XMLRPCDocGenerator -import sys - -from django.contrib.auth import authenticate -from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.http import HttpResponseServerError -from django.views.decorators.csrf import csrf_exempt -from django.urls import reverse -from django.utils import six -from django.utils.six.moves import xmlrpc_client -from django.utils.six.moves.xmlrpc_server import SimpleXMLRPCDispatcher - -from patchwork.models import Check -from patchwork.models import Patch -from patchwork.models import Person -from patchwork.models import Project -from patchwork.models import State -from patchwork.views.utils import patch_to_mbox - - -class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher, - XMLRPCDocGenerator): - - server_name = 'Patchwork XML-RPC API' - server_title = 'Patchwork XML-RPC API v1 Documentation' - - def __init__(self): - SimpleXMLRPCDispatcher.__init__(self, allow_none=False, - encoding=None) - XMLRPCDocGenerator.__init__(self) - - def _dumps(obj, *args, **kwargs): - kwargs['allow_none'] = self.allow_none - kwargs['encoding'] = self.encoding - return xmlrpc_client.dumps(obj, *args, **kwargs) - - self.dumps = _dumps - - # map of name => (auth, func) - self.func_map = {} - - def register_function(self, fn, auth_required): - self.funcs[fn.__name__] = fn # needed by superclass methods - self.func_map[fn.__name__] = (auth_required, fn) - - def _user_for_request(self, request): - auth_header = None - - if 'HTTP_AUTHORIZATION' in request.META: - auth_header = request.META.get('HTTP_AUTHORIZATION') - elif 'Authorization' in request.META: - auth_header = request.META.get('Authorization') - - if auth_header is None or auth_header == '': - raise Exception('No authentication credentials given') - - header = auth_header.strip() - - if not header.startswith('Basic '): - raise Exception('Authentication scheme not supported') - - header = header[len('Basic '):].strip() - - try: - decoded = base64.b64decode(header.encode('ascii')).decode('ascii') - username, password = decoded.split(':', 1) - except ValueError: - raise Exception('Invalid authentication credentials') - - return authenticate(username=username, password=password) - - def _dispatch(self, request, method, params): - if method not in list(self.func_map.keys()): - raise Exception('method "%s" is not supported' % method) - - auth_required, fn = self.func_map[method] - - if auth_required: - user = self._user_for_request(request) - if not user: - raise Exception('Invalid username/password') - - params = (user,) + params - - return fn(*params) - - def _marshaled_dispatch(self, request): - try: - params, method = six.moves.xmlrpc_client.loads(request.body) - - response = self._dispatch(request, method, params) - # wrap response in a singleton tuple - response = (response,) - response = self.dumps(response, methodresponse=1) - except six.moves.xmlrpc_client.Fault as fault: - response = self.dumps(fault) - except Exception: # noqa - # report exception back to server - response = self.dumps( - six.moves.xmlrpc_client.Fault( - 1, '%s:%s' % (sys.exc_info()[0], sys.exc_info()[1])), - ) - - return response - - -dispatcher = PatchworkXMLRPCDispatcher() - -# XMLRPC view function - - -@csrf_exempt -def xmlrpc(request): - if request.method not in ['POST', 'GET']: - return HttpResponseRedirect(reverse('project-list')) - - response = HttpResponse() - - if request.method == 'POST': - try: - ret = dispatcher._marshaled_dispatch(request) - except Exception: # noqa - return HttpResponseServerError() - else: - ret = dispatcher.generate_html_documentation() - - response.write(ret) - - return response - -# decorator for XMLRPC methods. Setting login_required to true will call -# the decorated function with a non-optional user as the first argument. - - -def xmlrpc_method(login_required=False): - def wrap(f): - dispatcher.register_function(f, login_required) - return f - - return wrap - - -# We allow most of the Django field lookup types for remote queries -LOOKUP_TYPES = ['iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', - 'in', 'startswith', 'istartswith', 'endswith', - 'iendswith', 'range', 'year', 'month', 'day', 'isnull'] - - -####################################################################### -# Helper functions -####################################################################### - -def project_to_dict(obj): - """Serialize a project object. - - Return a trimmed down dictionary representation of a Project - object which is safe to send to the client. For example: - - { - 'id': 1, - 'linkname': 'my-project', - 'name': 'My Project', - } - - Args: - Project object to serialize. - - Returns: - Serialized Project object. - """ - return { - 'id': obj.id, - 'linkname': obj.linkname, - 'name': obj.name, - } - - -def person_to_dict(obj): - """Serialize a person object. - - Return a trimmed down dictionary representation of a Person - object which is safe to send to the client. For example: - - { - 'id': 1, - 'email': 'joe.bloggs@example.com', - 'name': 'Joe Bloggs', - 'user': None, - } - - Args: - Person object to serialize. - - Returns: - Serialized Person object. - """ - - # Make sure we don't return None even if the user submitted a patch - # with no real name. XMLRPC can't marshall None. - if obj.name is not None: - name = obj.name - else: - name = obj.email - - return { - 'id': obj.id, - 'email': obj.email, - 'name': name, - 'user': six.text_type(obj.user).encode('utf-8'), - } - - -def patch_to_dict(obj): - """Serialize a patch object. - - Return a trimmed down dictionary representation of a Patch - object which is safe to send to the client. For example: - - { - 'id': 1 - 'date': '2000-12-31 00:11:22', - 'filename': 'Fix-all-the-bugs.patch', - 'msgid': '', - 'name': "Fix all the bugs", - 'project': 'my-project', - 'project_id': 1, - 'state': 'New', - 'state_id': 1, - 'archived': False, - 'submitter': 'Joe Bloggs ', - 'submitter_id': 1, - 'delegate': 'admin', - 'delegate_id': 1, - 'commit_ref': '', - 'hash': '', - } - - Args: - Patch object to serialize. - - Returns: - Serialized Patch object. - """ - return { - 'id': obj.id, - 'date': six.text_type(obj.date).encode('utf-8'), - 'filename': obj.filename, - 'msgid': obj.msgid, - 'name': obj.name, - 'project': six.text_type(obj.project).encode('utf-8'), - 'project_id': obj.project_id, - 'state': six.text_type(obj.state).encode('utf-8'), - 'state_id': obj.state_id, - 'archived': obj.archived, - 'submitter': six.text_type(obj.submitter).encode('utf-8'), - 'submitter_id': obj.submitter_id, - 'delegate': six.text_type(obj.delegate).encode('utf-8'), - 'delegate_id': obj.delegate_id or 0, - 'commit_ref': obj.commit_ref or '', - 'hash': obj.hash or '', - } - - -def state_to_dict(obj): - """Serialize a state object. - - Return a trimmed down dictionary representation of a State - object which is safe to send to the client. For example: - - { - 'id': 1, - 'name': 'New', - } - - Args: - State object to serialize. - - Returns: - Serialized State object. - """ - return { - 'id': obj.id, - 'name': obj.name, - } - - -def check_to_dict(obj): - """Return a trimmed down dictionary representation of a Check - object which is OK to send to the client.""" - return { - 'id': obj.id, - 'date': six.text_type(obj.date).encode('utf-8'), - 'patch': six.text_type(obj.patch).encode('utf-8'), - 'patch_id': obj.patch_id, - 'user': six.text_type(obj.user).encode('utf-8'), - 'user_id': obj.user_id, - 'state': obj.get_state_display(), - 'target_url': obj.target_url, - 'description': obj.description, - 'context': obj.context, - } - - -def patch_check_to_dict(obj): - """Return a combined patch check.""" - return { - 'state': obj.combined_check_state, - 'total': len(obj.checks), - 'checks': [check_to_dict(check) for check in obj.checks] - } - - -####################################################################### -# Public XML-RPC methods -####################################################################### - -def _get_objects(serializer, objects, max_count): - if max_count > 0: - return [serializer(x) for x in objects[:max_count]] - elif max_count < 0: - min_count = objects.count() + max_count - return [serializer(x) for x in objects[min_count:]] - else: - return [serializer(x) for x in objects] - - -@xmlrpc_method() -def pw_rpc_version(): - """Return Patchwork XML-RPC interface version. - - The API is versioned separately from patchwork itself. The API - version only changes when the API itself changes. As these changes - can include the removal or modification of methods, it is highly - recommended that one first test the API version for compatibility - before making method calls. - - History: - - 1.0.0: Patchwork 1.0 release - 1.1.0: ??? - 1.2.0: ??? - 1.3.0: Add support for negative indexing of Checks - - Returns: - Version of the API. - """ - return (1, 3, 0) - - -@xmlrpc_method() -def project_list(search_str=None, max_count=0): - """List projects matching a given linkname filter. - - Filter projects by linkname. Projects are compared to the search - string via a case-insensitive containment test, a.k.a. a partial - match. - - Args: - search_str: The string to compare project names against. If - blank, all projects will be returned. - max_count (int): The maximum number of projects to return. - - Returns: - A serialized list of projects matching filter, if any. A list - of all projects if no filter given. - """ - if search_str: - projects = Project.objects.filter(linkname__icontains=search_str) - else: - projects = Project.objects.all() - - return _get_objects(project_to_dict, projects, max_count) - - -@xmlrpc_method() -def project_get(project_id): - """Get a project by its ID. - - Retrieve a project matching a given project ID, if any exists. - - Args: - project_id (int): The ID of the project to retrieve. - - Returns: - The serialized project matching the ID, if any, else an empty - dict. - """ - try: - project = Project.objects.get(id=project_id) - return project_to_dict(project) - except Project.DoesNotExist: - return {} - - -@xmlrpc_method() -def person_list(search_str=None, max_count=0): - """List persons matching a given name or email filter. - - Filter persons by name and email. Persons are compared to the - search string via a case-insensitive containment test, a.k.a. a - partial match. - - Args: - search_str: The string to compare person names or emails - against. If blank, all persons will be returned. - max_count (int): The maximum number of persons to return. - - Returns: - A serialized list of persons matching filter, if any. A list - of all persons if no filter given. - """ - if search_str: - people = (Person.objects.filter(name__icontains=search_str) | - Person.objects.filter(email__icontains=search_str)) - else: - people = Person.objects.all() - - return _get_objects(person_to_dict, people, max_count) - - -@xmlrpc_method() -def person_get(person_id): - """Get a person by its ID. - - Retrieve a person matching a given person ID, if any exists. - - Args: - person_id (int): The ID of the person to retrieve. - - Returns: - The serialized person matching the ID, if any, else an empty - dict. - """ - try: - person = Person.objects.get(id=person_id) - return person_to_dict(person) - except Person.DoesNotExist: - return {} - - -@xmlrpc_method() -def patch_list(filt=None): - """List patches matching all of a given set of filters. - - Filter patches by one or more of the below fields: - - * id - * name - * project_id - * submitter_id - * delegate_id - * archived - * state_id - * date - * commit_ref - * hash - * msgid - - It is also possible to specify the number of patches returned via - a ``max_count`` filter. - - * max_count - - With the exception of ``max_count``, the specified field of the - patches are compared to the search string using a provided - field lookup type, which can be one of: - - * iexact - * contains - * icontains - * gt - * gte - * lt - * in - * startswith - * istartswith - * endswith - * iendswith - * range - * year - * month - * day - * isnull - - Please refer to the Django documentation for more information on - these field lookup types. - - An example filter would look like so: - - { - 'name__icontains': 'Joe Bloggs', - 'max_count': 1, - } - - Args: - filt (dict): The filters specifying the field to compare, the - lookup type and the value to compare against. Keys are of - format ``[FIELD_NAME]`` or ``[FIELD_NAME]__[LOOKUP_TYPE]``. - Example: ``name__icontains``. Values are plain strings to - compare against. - - Returns: - A serialized list of patches matching filters, if any. A list - of all patches if no filter given. - """ - if filt is None: - filt = {} - - # We allow access to many of the fields. But, some fields are - # filtered by raw object so we must lookup by ID instead over - # XML-RPC. - ok_fields = [ - 'id', - 'name', - 'project_id', - 'submitter_id', - 'delegate_id', - 'archived', - 'state_id', - 'date', - 'commit_ref', - 'hash', - 'msgid', - 'max_count', - ] - - dfilter = {} - max_count = 0 - - for key in filt: - parts = key.split('__') - if parts[0] not in ok_fields: - # Invalid field given - return [] - if len(parts) > 1 and LOOKUP_TYPES.count(parts[1]) == 0: - # Invalid lookup type given - return [] - - try: - if parts[0] == 'project_id': - dfilter['project'] = Project.objects.get(id=filt[key]) - elif parts[0] == 'submitter_id': - dfilter['submitter'] = Person.objects.get(id=filt[key]) - elif parts[0] == 'delegate_id': - dfilter['delegate'] = Person.objects.get(id=filt[key]) - elif parts[0] == 'state_id': - dfilter['state'] = State.objects.get(id=filt[key]) - elif parts[0] == 'max_count': - max_count = filt[key] - else: - dfilter[key] = filt[key] - except (Project.DoesNotExist, Person.DoesNotExist, State.DoesNotExist): - # Invalid Project, Person or State given - return [] - - patches = Patch.objects.filter(**dfilter) - - # Only extract the relevant fields. This saves a big db load as we - # no longer fetch content/headers/etc for potentially every patch - # in a project. - patches = patches.defer('content', 'headers', 'diff') - - return _get_objects(patch_to_dict, patches, max_count) - - -@xmlrpc_method() -def patch_get(patch_id): - """Get a patch by its ID. - - Retrieve a patch matching a given patch ID, if any exists. - - Args: - patch_id (int): The ID of the patch to retrieve - - Returns: - The serialized patch matching the ID, if any, else an empty - dict. - """ - try: - patch = Patch.objects.get(id=patch_id) - return patch_to_dict(patch) - except Patch.DoesNotExist: - return {} - - -@xmlrpc_method() -def patch_get_by_hash(hash): # noqa - """Get a patch by its hash. - - Retrieve a patch matching a given patch hash, if any exists. - - Args: - hash: The hash of the patch to retrieve - - Returns: - The serialized patch matching the hash, if any, else an empty - dict. - """ - try: - patch = Patch.objects.get(hash=hash) - return patch_to_dict(patch) - except Patch.DoesNotExist: - return {} - - -@xmlrpc_method() -def patch_get_by_project_hash(project, hash): - """Get a patch by its project and hash. - - Retrieve a patch matching a given project and patch hash, if any - exists. - - Args: - project (str): The project of the patch to retrieve. - hash: The hash of the patch to retrieve. - - Returns: - The serialized patch matching both the project and the hash, - if any, else an empty dict. - """ - try: - patch = Patch.objects.get(project__linkname=project, - hash=hash) - return patch_to_dict(patch) - except Patch.DoesNotExist: - return {} - - -@xmlrpc_method() -def patch_get_mbox(patch_id): - """Get a patch by its ID in mbox format. - - Retrieve a patch matching a given patch ID, if any exists, and - return in mbox format. - - Args: - patch_id (int): The ID of the patch to retrieve. - - Returns: - The serialized patch matching the ID, if any, in mbox format, - else an empty string. - """ - try: - patch = Patch.objects.get(id=patch_id) - return patch_to_mbox(patch) - except Patch.DoesNotExist: - return '' - - -@xmlrpc_method() -def patch_get_diff(patch_id): - """Get a patch by its ID in diff format. - - Retrieve a patch matching a given patch ID, if any exists, and - return in diff format. - - Args: - patch_id (int): The ID of the patch to retrieve. - - Returns: - The serialized patch matching the ID, if any, in diff format, - else an empty string. - """ - try: - patch = Patch.objects.get(id=patch_id) - return patch.diff - except Patch.DoesNotExist: - return '' - - -@xmlrpc_method(login_required=True) -def patch_set(user, patch_id, params): - """Set fields of a patch. - - Modify a patch matching a given patch ID, if any exists, and using - the provided ``key,value`` pairs. Only the following parameters may - be set: - - * state - * commit_ref - * archived - - Any other field will be rejected. - - **NOTE:** Authentication is required for this method. - - Args: - user (User): The user making the request. This will be - populated from HTTP Basic Auth. - patch_id (int): The ID of the patch to modify. - params (dict): A dictionary of keys corresponding to patch - object fields and the values that said fields should be - set to. - - Returns: - True, if successful else raise exception. - - Raises: - Exception: User did not have necessary permissions to edit this - patch - Patch.DoesNotExist: The patch did not exist. - """ - ok_params = ['state', 'commit_ref', 'archived'] - - patch = Patch.objects.get(id=patch_id) - - if not patch.is_editable(user): - raise Exception('No permissions to edit this patch') - - for (k, v) in params.items(): - if k not in ok_params: - continue - - if k == 'state': - patch.state = State.objects.get(id=v) - - else: - setattr(patch, k, v) - - patch.save() - - return True - - -@xmlrpc_method() -def state_list(search_str=None, max_count=0): - """List states matching a given name filter. - - Filter states by name. States are compared to the search string - via a case-insensitive containment test, a.k.a. a partial match. - - Args: - search_str: The string to compare state names against. If - blank, all states will be returned. - max_count (int): The maximum number of states to return. - - Returns: - A serialized list of states matching filter, if any. A list - of all states if no filter given. - """ - if search_str: - states = State.objects.filter(name__icontains=search_str) - else: - states = State.objects.all() - - return _get_objects(state_to_dict, states, max_count) - - -@xmlrpc_method() -def state_get(state_id): - """Get a state by its ID. - - Retrieve a state matching a given state ID, if any exists. - - Args: - state_id: The ID of the state to retrieve. - - Returns: - The serialized state matching the ID, if any, else an empty - dict. - """ - try: - state = State.objects.get(id=state_id) - return state_to_dict(state) - except State.DoesNotExist: - return {} - - -@xmlrpc_method() -def check_list(filt=None): - """List checks matching all of a given set of filters. - - Filter checks by one or more of the below fields: - - * id - * user - * project_id - * patch_id - - It is also possible to specify the number of patches returned via - a ``max_count`` filter. - - * max_count - - With the exception of ``max_count``, the specified field of the - patches are compared to the search string using a provided - field lookup type, which can be one of: - - * iexact - * contains - * icontains - * gt - * gte - * lt - * in - * startswith - * istartswith - * endswith - * iendswith - * range - * year - * month - * day - * isnull - - Please refer to the Django documentation for more information on - these field lookup types. - - An example filter would look like so: - - { - 'user__icontains': 'Joe Bloggs', - 'max_count': 1, - } - - Args: - filt (dict): The filters specifying the field to compare, the - lookup type and the value to compare against. Keys are of - format ``[FIELD_NAME]`` or ``[FIELD_NAME]__[LOOKUP_TYPE]``. - Example: ``name__icontains``. Values are plain strings to - compare against. - - Returns: - A serialized list of Checks matching filters, if any. A list - of all Checks if no filter given. - """ - if filt is None: - filt = {} - - # We allow access to many of the fields. But, some fields are - # filtered by raw object so we must lookup by ID instead over - # XML-RPC. - ok_fields = [ - 'id', - 'user', - 'project_id', - 'patch_id', - 'max_count', - ] - - dfilter = {} - max_count = 0 - - for key in filt: - parts = key.split('__') - if parts[0] not in ok_fields: - # Invalid field given - return [] - if len(parts) > 1: - if LOOKUP_TYPES.count(parts[1]) == 0: - # Invalid lookup type given - return [] - - if parts[0] == 'user_id': - dfilter['user'] = Person.objects.filter(id=filt[key])[0] - if parts[0] == 'project_id': - dfilter['patch__project'] = Project.objects.filter( - id=filt[key])[0] - elif parts[0] == 'patch_id': - dfilter['patch'] = Patch.objects.filter(id=filt[key])[0] - elif parts[0] == 'max_count': - max_count = filt[key] - else: - dfilter[key] = filt[key] - - checks = Check.objects.filter(**dfilter) - - return _get_objects(check_to_dict, checks, max_count) - - -@xmlrpc_method() -def check_get(check_id): - """Get a check by its ID. - - Retrieve a check matching a given check ID, if any exists. - - Args: - check_id (int): The ID of the check to retrieve - - Returns: - The serialized check matching the ID, if any, else an empty - dict. - """ - try: - check = Check.objects.get(id=check_id) - return check_to_dict(check) - except Check.DoesNotExist: - return {} - - -@xmlrpc_method(login_required=True) -def check_create(user, patch_id, context, state, target_url="", - description=""): - """Add a Check to a patch. - - **NOTE:** Authentication is required for this method. - - Args: - patch_id (id): The ID of the patch to create the check against. - context: Type of test or system that generated this check. - state: "pending", "success", "warning", or "fail" - target_url: Link to artifact(s) relating to this check. - description: A brief description of the check. - - Returns: - True, if successful else raise exception. - """ - patch = Patch.objects.get(id=patch_id) - if not patch.is_editable(user): - raise Exception('No permissions to edit this patch') - for state_val, state_str in Check.STATE_CHOICES: - if state == state_str: - state = state_val - break - else: - raise Exception("Invalid check state: %s" % state) - Check.objects.create(patch=patch, context=context, state=state, user=user, - target_url=target_url, description=description) - return True - - -@xmlrpc_method() -def patch_check_get(patch_id): - """Get a patch's combined checks by its ID. - - Retrieve a patch's combined checks for the patch matching a given - patch ID, if any exists. - - Args: - patch_id (int): The ID of the patch to retrieve checks for - - Returns: - The serialized combined patch checks matching the ID, if any, - else an empty dict. - """ - try: - patch = Patch.objects.get(id=patch_id) - return patch_check_to_dict(patch) - except Patch.DoesNotExist: - return {} diff --git releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml new file mode 100644 index 000000000000..e86c156844d8 --- /dev/null +++ releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + To simplify the code base, the XML-RPC API has been removed. This + means that `pwclient` will no longer work. + + As a result, `tools/patchwork-update-commits` and + `tools/post-receive.hook` have also been removed. + + Users are encouraged to try + `git-pw `_. diff --git tools/patchwork-update-commits tools/patchwork-update-commits deleted file mode 100755 index 269dac9eee4e..000000000000 --- tools/patchwork-update-commits +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# -# Patchwork - automated patch tracking system -# Copyright (C) 2010 Jeremy Kerr -# -# SPDX-License-Identifier: GPL-2.0-or-later - -TOOLS_DIR="$(dirname "$0")" -PW_DIR="${TOOLS_DIR}/../patchwork" - -if [ "$#" -lt 1 ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -git rev-list --reverse "$@" | -while read -r commit; do - hash=$(git diff "$commit~..$commit" | python "$PW_DIR/hasher.py") - pwclient update -s Accepted -c "$commit" -h "$hash" -done diff --git tools/post-receive.hook tools/post-receive.hook deleted file mode 100755 index 9f2f0503d7ee..000000000000 --- tools/post-receive.hook +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash - -# Patchwork - automated patch tracking system -# Copyright (C) 2010 martin f. krafft -# -# SPDX-License-Identifier: GPL-2.0-or-later - -# Git post-receive hook to update Patchwork patches after Git pushes -set -eu - -PW_DIR=/opt/patchwork/patchwork - -#TODO: the state map should really live in the repo's git-config -STATE_MAP="refs/heads/master:Accepted" - -# ignore all commits already present in these refs -# e.g., -# EXCLUDE="refs/heads/upstream refs/heads/other-project" -EXCLUDE="" - -do_exit=0 -trap "do_exit=1" INT - -get_patchwork_hash() { - local hash - hash=$(git diff "$1~..$1" | python $PW_DIR/hasher.py) - echo "$hash" - test -n "$hash" -} - -get_patch_id() { - local id - id=$(pwclient info -h "$1" 2>/dev/null | \ - sed -rne 's,- id[[:space:]]*: ,,p') - echo "$id" - test -n "$id" -} - -set_patch_state() { - pwclient update -s "$2" -c "$3" "$1" 2>&1 -} - -update_patches() { - local cnt; cnt=0 - for rev in $(git rev-parse --not ${EXCLUDE} | - git rev-list --stdin --no-merges --reverse "${1}".."${2}"); do - if [ "$do_exit" = 1 ]; then - echo "I: exiting..." >&2 - break - fi - hash=$(get_patchwork_hash "$rev") - if [ -z "$hash" ]; then - echo "E: failed to hash rev $rev." >&2 - continue - fi - id=$(get_patch_id "$hash" || true) - if [ -z "$id" ]; then - echo "E: failed to find patch for rev $rev." >&2 - continue - fi - reason="$(set_patch_state "$id" "$3" "$rev")" - if [ -n "$reason" ]; then - echo "E: failed to update patch #$id${reason:+: $reason}." >&2 - continue - fi - echo "I: patch #$id updated using rev $rev." >&2 - cnt=$((cnt + 1)) - done - - echo "I: $cnt patch(es) updated to state $3." >&2 -} - -while read -r oldrev newrev refname; do - found=0 - for i in $STATE_MAP; do - key="${i%:*}" - if [ "$key" = "$refname" ]; then - update_patches "$oldrev" "$newrev" ${i#*:} - found=1 - break - fi - done - if [ $found -eq 0 ]; then - echo "E: STATE_MAP has no mapping for branch $refname" >&2 - fi -done